吴昊品游戏核心算法(新年特别篇)—— 在你打麻将快要胡牌却不知道胡哪个字的时候可以用这个AI插件(模拟)
图片选自著名的日本动画片——《天才麻将少女》
全 国各地有不同的麻将,相同的麻将在不同的地域有着不同的规则,这不,今年过年,我就体会到了这一点。我爸爸的老家在湖南的长沙,而我妈的老家在湖南的邵 阳,虽然这两个城市都在湖南,但是由于地域的差异,长沙麻将中的“将”只能取为二,五,八,而邵阳麻将则可以乱来,什么都可以作为将(这个貌似也随着时间 的变化而变化,毕竟,时代在进步,规则也会随着改变)。我在《吴昊教你打麻将》中已经抽取CSDN的Source并补上了“七小队”和“十三幺”的胡牌办 法,这个插件,可以让你在拥有13张牌并且无限接近于胡牌(这个的术语我搞忘叫什么了,貌似长沙话好像是nuo-ting吧)的情况下,找到尽可能多的胡 牌方法(再任意将的牌中,这样的选择还是很多的,有时候甚至有10个左右的办法,只要你的牌尽可能“SUPER”)
这个插件是用朴素的暴力算法实现的,虽然“很暴力”,但由于每个人所抓的牌的总数不算多(加上胡牌的那一张也就只有14张),所以即使是暴力,也不会消耗多长时间(普通的电脑1s内是足够的了),我们这里重新模拟一遍,并感悟一下check()函数的奥义。
这里,我们为了简单起见,不考虑七小队和十三幺以及天胡,地胡等特殊的胡牌方式,实际上,对于智能取二五八为将的牌,也是非常好改的(加上条件判断就可以了)。
现在,我们再来明确一下具体的规则(Source:HDOJ 3391),只要我们可以满足“4组+1对”,我们就赢了,其中,4组既可以是三张连续的(麻将术语为“吃”),也可以是三张相同的(麻将术语为 “碰”),吃吃碰碰,再加上一对“将”,就是胡牌的不二法门。这里,我们假设13张牌中,如果再补上一张牌就可以胡牌叫做“报警”,而即使补上一张也不行 的话,就只有输出“NONE”了。
以上是万字(Character),我们这里用1c表示一万
以上是筒字(Stone),我们这里用1s表示一筒
以上是条字(Bamboo),我们这里用1b表示一条
输入输出就不给出了,这里给出麻将的整个Source,以及亮点(Highlights,主要是check()函数的妙用)
2 Highlights:
3 (1)check()函数,套用fun函数,判断每一组牌是否可以组成1对4组
4 (2)为了配合check()函数,在主函数中加入一个对种类牌为3的倍数时的剪枝
5 (3)在fun()函数中,分别对吃和碰两种情况进行讨论
6 (4)利用f[]数组,将int类型的数字重新转换为麻将的三个种类之一(万,筒,条)——f[ans[i]%10]
7 */
8
9 #include<stdio.h>
10
11 //数组尽量开了一点,每一个类型的牌有9张,这里数组开到15
12 int D[4][15];
13
14 int fun(int *b)
15 {
16 int i;
17 //如果都找完了,就返回
18 if(b[0]==0)return 1;
19 //找到第一张
20 for(i=1;i<=9;i++)
21 {
22 if(b[i])
23 break;
24 }
25 if(b[i]>=3)
26 {
27 //如果某一张牌大于等于三张的话
28 b[i]-=3;
29 b[0]-=3;
30 //由于每一种类型的牌都只考虑在四张以内的情况,所以三张如果相同,还有一张的话,会形成孤立
31 if(fun(b)) return 1;
32 b[i]+=3;
33 b[0]+=3;
34 }
35 if(i<=7)
36 {
37 //拿掉最小牌i,和i+1,i+2 能配成n-1组吗?
38 if(b[i+1]*b[i+2])
39 {
40 b[i]--;b[i+1]--;b[i+2]--; b[0]-=3;
41 //同样是这个道理
42 if(fun(b)) return 1;
43 b[i]++;b[i+1]++;b[i+2]++; b[0]+=3;
44 }
45
46 }
47 return 0;
48 }
49
50 int check()
51 {
52 int i,j,k,s,a[15];
53 for(i=1;i<=3;i++)
54 {
55 if(D[i][0]==0) continue;
56 //考虑已经组好的情况
57 if(D[i][0]%3==0)
58 {
59 //i色牌的张数 是3的倍数,能配成 D[i][0]/3组吗?
60 for(j=0;j<=9;j++) a[j]=D[i][j];
61 if(fun(a)==0) return 0;
62 }
63 else
64 {
65 for(j=0;j<=9;j++) a[j]=D[i][j];
66 //i色牌的张数是3n+2,拿掉相同2张,如果只有一张牌的话,就铁定不行了
67 for(j=1;j<=9;j++)
68 {
69 if(a[j]>=2)
70 {
71 a[j]-=2;a[0]-=2;
72 //i色牌的张数 是3n+2,拿掉1对 能配成 (D[i][0]-2)/3组吗?
73 if(fun(a)==1) break;
74 a[j]+=2;a[0]+=2;
75 }
76 }
77 if(j>9) return 0;
78 }
79 }
80 return 1;
81 }
82
83 int main()
84 {
85 int T,i,j,k,ca=0,a[5],num,ans[30];
86 char ch[5],f[]={'0','s','b','c'};
87 scanf("%d",&T);
88 while (T--)
89 {
90 for(i=1;i<=3;i++)
91 for(j=0;j<=10;j++)
92 D[i][j]=0;
93 for(i=1;i<=13;i++)
94 {
95 //这13张牌中,第0列记录总数而之后的九列分别记录每一张牌的值
96 scanf("%s",ch); j=ch[0]-'0';
97 if(ch[1]=='s'){D[1][0]++;D[1][j]++;}
98 if(ch[1]=='b'){D[2][0]++;D[2][j]++;}
99 if(ch[1]=='c'){D[3][0]++;D[3][j]++;}
100 }
101 printf("Case %d:",++ca);
102 //为什么只有这一种情况呢?主要是考虑到有将的原因,如果胡的牌不是将的话,那么可能存在两张牌都不是3的倍数的情况
103 if((D[1][0]%3) &&(D[2][0]%3) &&(D[3][0]%3))
104 {
105 //如3种牌的张数都不是3的倍数,无解
106 printf(" None\n");
107 continue;
108 }
109 //有 13张麻将牌 D[][]
110 num=0;
111 for(i=1;i<=3;i++)
112 {
113 if(D[i][0]%3==0)
114 {
115 //第i色麻将有3n张,不增加,判断能否构成n组,这里将其重新导入到一个a[]中
116 for(j=0;j<=9;j++)
117 a[j]=D[i][j];
118 if(fun(a)==0) break; //不能,就结束
119 D[i][0]=0; // 能,删除3n张牌,以后就不判断了
120 continue;
121 }
122 for(k=1;k<=9 ;k++)
123 {
124 //这种牌有4张,不能增加,这是题目的限制,我们只考虑4张以及以下的情况
125 if(D[i][k]>=4) continue;
126 //增加i色牌k,D[][]有14张
127 D[i][k]++;
128 D[i][0]++;
129 if(check()) // 这14张 能配成 1对 4组
130 {
131 //这里的代码可以重构一下,改成如下形式:ans[num++]=k*10+i
132 num++;
133 ans[num]=k*10+i;
134 }
135 //如果不符合条件的话,将这一张牌取消,继续暴力
136 D[i][k]--;
137 D[i][0]--;
138 }
139 }
140 //输出所有的情况
141 for(i=1;i<=num;i++)
142 //从这里可以看出f[]的使用主要是将int的值转化成麻将的类型值
143 printf(" %d%c",ans[i]/10,f[ans[i]%10]);
144 if(num==0)
145 printf(" None");
146 printf("\n");
147 }
148 return 0;
149 }
150
151