麻将,风靡大江南北,今儿让笔者和大家一起看看麻将中的算法题
中国麻将(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张作为刻子或者顺子。如下图所示,即为一次递归求解的过程。
选将有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;
}