说说算法题的那些事儿(3)~麻将算法题

麻将,风靡大江南北,今儿让笔者和大家一起看看麻将中的算法题

中国麻将(Chinese Mahjong, UVa 11210)
麻将是一个中国原创的4人玩的游戏。这个游戏有很多变种,但本题只考虑一种有136张牌的玩法。

这136张牌所包含的内容如下。
饼(筒)牌:每张牌包括一系列点,每个点代表一个铜钱,如图所示。本题中用1T、2T、3T、4T、5T、6T、7T、8T、9T表示。

这里写图片描述

索(条)牌:每张牌由一系列竹棍组成,每根棍代表一挂铜钱,如图所示。本题中用1S、2S、3S、4S、5S、6S、7S、8S、9S表示
这里写图片描述

万牌:每张牌代表一万枚铜钱,如图所示。本题中用1W、2W、3W、4W、5W、6W、7W、8W、9W表示。
这里写图片描述

风牌:东、南、西、北风,如图所示。本题中用DONG、NAN、XI、BEI表示。
这里写图片描述
箭牌:中、发、白,如图所示。本题中用ZHONG、FA、BAI表示
这里写图片描述

总共有9×3+4+3=34种牌,每种4张,一共有136张牌。
其实麻将中还有如图所示的8张花牌,所以共有136 + 8 = 144张牌,但是本题中不予考虑。
这里写图片描述

中国麻将的规则十分复杂,本题中只需考虑部分规则。在本题中,手牌(即每个人手里的牌)总是有13张。如果多了某张牌以后,整副牌可以拆成一个将(两张相同的牌)、0个或多个刻子(3张相同的牌)和0个或多个顺子(3张同花相连的牌。注意,风牌和箭牌不能形成顺子),我们就说这手牌“听”这张牌,即拿到那张牌以后就赢了,称为“和”(实战中还要考虑番数和特殊和法,在本题中可以忽略)。
比如,如图所示的这手牌:
这里写图片描述

听牌、和,即1S、FA和4S。听牌的原因是:“发”做将,另有3个顺子(1S2S3S, 1S2S3S, 2S3S4S)。
【输入格式】
输入数据最多50组。每组数据由一行13张牌给出,输入保证给出的牌是合法的。输入结束标记为一行单个0。
【输出格式】
对于每组数据,输出所有“听”的牌,按照描述中的顺序列出(1T-9T,1S-9S,1W-9W,DONG,NAN,XI,BEI,ZHONG,FA,BAI)。每张牌最多被列出一次。如果没有“听”牌,输出Not ready。
【分析】
如果您和笔者一样对麻将很熟悉,不妨回忆一下自己平时打麻将时,是如何知道自己有没有听牌的。虽然多数情况都容易判断,但对于一些复杂的情况,新手容易看不出自己“听”牌了,或者看不全所有“听”的牌,而麻将老手却可以。原因在于,麻将老手擅长把手里的牌按照不同的方式进行组合。在程序里,我们也需要用一点“暴力”来枚举所有可能的组合方式。
一共只有34种牌,因此可以依次判断是否“听”这些牌。比如,为了判断是否“听”一万,只需要判断自己拿到这张一万后是否可以和牌。这样,问题就转化为了:给定14张牌,判断是否可以和牌。为此,我们可以递归求解:首先选两张牌作为“将”,然后每次选3张作为刻子或者顺子。如下图所示,即为一次递归求解的过程。
说说算法题的那些事儿(3)~麻将算法题_第1张图片

选将有5种方法(一二三四五万都可以做将)。如果选五万做将,一万要么属于一个刻子,要么属于一个顺子(二三四)。注意,这时不必考虑其他牌是如何形成刻子或者顺子的,否则会出现重复枚举(想一想,为什么)。
为了快速选出将、刻子和顺子,我们用一个34维向量来表示状态,即每种牌所剩的张数。除了第一次直接枚举将牌之外,每次只需要考虑编号最小的牌,看它能否形成刻子或者顺子(一定是以它作为最小牌。想一想,为什么),并且递归判断。本题唯一的陷阱是:每一种牌都只有4张,所以1S1S1S1S是不“听”任何牌的。
完整代码如下。

#include
#include

const char* mahjong[] = {
"1T","2T","3T","4T","5T","6T","7T","8T","9T",
"1S","2S","3S","4S","5S","6S","7S","8S","9S",
"1W","2W","3W","4W","5W","6W","7W","8W","9W",
"DONG","NAN","XI","BEI",
"ZHONG","FA","BAI"
};

int convert(char *s){                       //只在预处理时调用,因此速度无关紧要
  for(int i = 0; i < 34; i++)
    if(strcmp(mahjong[i], s) == 0) return i;
  return -1;
}

int c[34];
bool search(int dep){                       //回溯法递归过程
  int i;
  for(i = 0; i < 34; i++) if (c[i] >= 3){   //刻子
    if(dep == 3) return true; 
    c[i] -= 3; 
    if(search(dep+1)) return true; 
    c[i] += 3;
  }
  for(i = 0; i <= 24; i++) if (i % 9 <= 6 && c[i] >= 1 && c[i+1] >= 1 && c[i+2] >= 1){                                          //顺子
    if(dep == 3) return true; 
    c[i]--; c[i+1]--; c[i+2]--;
    if(search(dep+1)) return true; 
    c[i]++; c[i+1]++; c[i+2]++;
  }
  return false;
}

bool check(){
  int i;
  for(i = 0; i < 34; i++)
    if(c[i] >= 2){                          //将牌
      c[i] -= 2;
      if(search(0)) return true;
      c[i] += 2;
  }
  return false;
}

int main(){
  int caseno = 0, i, j;
  bool ok;
  char s[100];
  int mj[15];

  while(scanf("%s", &s) == 1){
    if(s[0] == '0') break;
    printf("Case %d:", ++caseno);
    mj[0] = convert(s);
    for(i = 1; i < 13; i++){
      scanf("%s", &s);
      mj[i] = convert(s);
    }
    ok = false;
    for(i = 0; i < 34; i++){
      memset(c, 0, sizeof(c));
      for(j = 0; j < 13; j++) c[mj[j]]++;
      if(c[i] >= 4) continue;   //每种牌最多只有4张
      c[i]++;                   //假设拥有这张牌
      if(check()){              //如果“和”了
        ok = true;              //说明听这张牌
        printf(" %s", mahjong[i]);
      }
      c[i]--;
    }
    if(!ok) printf(" Not ready");
    printf("\n");
  }
  return 0;
}

你可能感兴趣的:(数学问题,算法)