吴昊品游戏核心算法 Round 3 —— 速算24点AI(HDOJ 1427)

 如图所示,这是一款基于android的24点游戏,输入四个整数(1--13)之后,点击计算之后,计算机就可以自动给出所有可能的答案。

 

  24点是把4个整数(一般是正整数)通过加减乘除运算,使最后的计算结果是24的一个数学游戏,可以考验人的智力和数学敏感性。通常是使用扑克牌来进行游 戏的,一副牌中抽去大小王后还剩下52张(如果初练也可只用1~10这40张牌),任意抽取4张牌(称为牌组),用加、减、乘、除 (可加括号)把牌面上的数算成24。每张牌必须且只能用一次,如抽出的牌是3、8、8、9,那么算式为(9—8)×8×3或3×8÷(9—8)或(9— 8÷8)×3等。

  它始于何年何月已无从考究,但它以自己独具的数学魅力和丰富的内涵正逐渐被越来越多的人们所接受。这种游戏方式简单易学,能健脑益智,是一项极为有益的活动。

  对于24点的计算,可以有如下的一些技巧:

  1.利用3×8=24、4×6=24 、12×2=24求解。

把牌面上的四个数想办法凑成3和8、4和6、12和2再相乘求解。如3、3、6、10可组成(10-6÷3)×3=24或(10-3-3)×6=24。又如2、3、3、7可组成(7+3-2)×3=24等。实践证明,这种方法是利用率最大、命中率最高的一种方法。

2.利用0、1的运算特性求解。

如3、4、4、8可组成3×8+4—4=24等。又如4、5、J、K可组成11×(5—4)+13=24等。

3.在有解的牌组中,用得最为广泛的是以下六种解法:(我们用a、b、c、d表示牌面上的四个数)

①(a—b)×(c+d)

如(10—4)×(2+2)=24等。

②(a+b)÷c×d

如(10+2)÷2×4=24等。

③(a-b÷c)×d

如(3—2÷2)×12=24等。

④(a+b-c)×d

如(9+5—2)×2=24等。

⑤a×b+c—d

如11×3+l—10=24等。

⑥(a-b)×c+d

如(4—l)×6+6=24等。

  此外,关于更多的24点的扩展(主要是基于组合数学+概率的一些延伸,我会在最后补充)

  下面,我来给出HDOJ 1427(杭电OJ)中关于此问题的描述,并给出并品析其AI算法的精髓。

  Problem Description

速算24点相信绝大多数人都玩过。就是随机给你四张牌,包括A(1),2,3,4,5,6,7,8,9,10,J(11),Q(12),K(13)。要求只 用'+','-','*','/'运算符以及括号改变运算顺序,使得最终运算结果为24(每个数必须且仅能用一次)。游戏很简单,但遇到无解的情况往往让 人很郁闷。你的任务就是针对每一组随机产生的四张牌,判断是否有解。我们另外规定,整个计算过程中都不能出现小数(吴昊评注:这一个条件并不是必要的,尤其在于一些只有唯一解的24点解法中)。

Input

每组输入数据占一行,给定四张牌。

Output

每一组输入数据对应一行输出。如果有解则输出"Yes",无解则输出"No"。

以上,我给出了原问题,可以说,和各地的麻将的玩法都不同一回事,24点也有各种玩法,其AI代码也是大同小异,究其精髓,还是最最不同的搜索,不过,搜索的过程中,要注意,按照顺序搜索,先搜两张牌的结果,再混为三张牌,最后是四张牌,不要用暴力!这样的AI木有效率!

关 于源代码的品析我都已经加入到注释上了,我觉得Cal函数起到了计算器的作用,而CalThree和CalTwo分的两种情况也很全面。至于 CalFour,则起到一个总括的作用(源代码在四个数无法组成24时编译器会报错,但是我反复检验源码认为木有问题,求DEBUG)。


  1 #include<stdio.h>
  2 
  3  #include<stdlib.h>
  4 
  5  #include<string.h>//开字符串库,处理输入输出
  6 
  7  
  8 
  9  int num[4];
 10 
 11  
 12 
 13  int Cal(int a,int b,int n) //两个数的加减乘除过程
 14 
 15  {
 16 
 17    switch(n)
 18 
 19    {
 20 
 21      case 0return a+b;
 22 
 23      case 1return a*b;
 24 
 25      case 2if(a>b) return a-b;
 26 
 27              else return b-a;
 28 
 29      case 3if(a>=b&&b!=0&&a%b==0return a/b;
 30 
 31              else if(a<b&&a!=b&&b%a==0return b/a;
 32 
 33              else return -1//如果没有答案,就返回-1        
 34 
 35    }   
 36 
 37    return -1//这也是为了增强鲁棒性
 38 
 39  }
 40 
 41  
 42 
 43  bool CalThree(int a,int b,int as)
 44 
 45  {
 46 
 47    int i,j,temp;
 48 
 49    int k=0,re[2];//再抓起最后的两张牌
 50 
 51    for(i=0;i<4;i++)
 52 
 53    {
 54 
 55      if((i==a)||(i==b)) continue;
 56 
 57      re[k++]=i;//数组里面自增是个不错的习惯,如果每次增加的时机都不确定的话                 
 58 
 59    }
 60 
 61    for(i=0;i<4;i++) //同理,再对四种运算进行处理
 62 
 63    {
 64 
 65      temp=Cal(as,num[re[0]],i);
 66 
 67      if(temp==-1continue;
 68 
 69      for(j=0;j<4;j++) //再取一张牌,看是否匹配24
 70 
 71      {
 72 
 73        if(Cal(temp,num[re[1]],j)==24)
 74 
 75          return true;               
 76 
 77      }                
 78 
 79    }      
 80 
 81    for(i=0;i<4;i++)//也有可能是先算第二张再算第一张
 82 
 83    {
 84 
 85      temp=Cal(as,num[re[1]],i);
 86 
 87      if(temp==-1continue;
 88 
 89      for(j=0;j<4;j++)
 90 
 91      {
 92 
 93        if(Cal(temp,num[re[0]],j)==24)
 94 
 95          return true;               
 96 
 97      }                                               
 98 
 99    }
100 
101    return false;
102 
103  }
104 
105  
106 
107  bool CalTwo(int a,int b,int as)
108 
109  {
110 
111    int i,j,k=0,re[2];
112 
113    int temp;
114 
115    for(i=0;i<4;i++)
116 
117    {
118 
119      if(i==a||i==b) continue;
120 
121      re[k++]=i;               
122 
123    }    
124 
125    for(i=0;i<4;i++)
126 
127    {
128 
129      temp=Cal(num[re[0]],num[re[1]],i);
130 
131      if(temp==-1continue;
132 
133      for(j=0;j<4;j++)//同上,测试四种运算
134 
135      {
136 
137        if(Cal(as,temp,j)==24)
138 
139          return true;               
140 
141      }               
142 
143    }
144 
145    return false;
146 
147  }
148 
149  
150 
151  bool CalFour(int n)
152 
153  {
154 
155    int i,j,temp;
156 
157    bool flag;
158 
159    for(i=0;i<4;i++)//再抽取一张牌,这张牌和以上的牌应该不同
160 
161    {
162 
163      if(i==n) continue;
164 
165      for(j=0;j<4;j++)//对于以上两张牌,利用不同的运算符号进行计算
166 
167      {
168 
169        temp=Cal(num[n],num[i],j);
170 
171        if(temp==-1continue;
172 
173        flag=CalThree(n,i,temp);//第一类情况,第三张与前面两张相混合
174 
175        if(flag) return true;
176 
177        flag=CalTwo(n,i,temp);//第二类情况,第三张与前面两张不互相混合
178 
179        if(flag) return true;               
180 
181      }               
182 
183    }
184 
185    return false;    
186 
187  }
188 
189  
190 
191  int main()
192 
193  {
194 
195    int i;
196 
197    char ch[10][5];//数组开大一点,可以增强鲁棒性,有可能保存几个空格的情况
198 
199    bool ans;
200 
201    //freopen(“f:\\1427.in”,”r”,stdin);
202 
203    //freopen(“f:\\1427.txt”,”w”,stdout);
204 
205    while(scanf("%s%s%s%s",ch[0],ch[1],ch[2],ch[3]) != EOF)
206 
207    {
208 
209      for(i=0;i<4; i++) //将四张输入的牌取出
210 
211      {
212 
213        if(ch[i][0]== 'A')
214 
215          num[i] =1;
216 
217        else if(ch[i][0] == 'J')
218 
219          num[i] =11;
220 
221        else if(ch[i][0] == 'Q')
222 
223          num[i] =12;
224 
225        else if(ch[i][0] == 'K')
226 
227          num[i] =13;
228 
229        else if(ch[i][0] == '1' && strlen(ch[i])== 2)//处理两个字符的情况
230 
231          num[i] =10;
232 
233        else
234 
235          num[i] =ch[i][0]-48;//将ASC码转为整数
236 
237      }
238 
239      ans =false;
240 
241      for(i=0;i<4; i++)
242 
243      {
244 
245        ans =CalFour(i);//判定过程,i表示先抽取一张牌
246 
247        if(ans)//由于这里只需要判定是否可以组成24点,故加此判定可以起到剪枝的效果
248 
249          break;
250 
251      }
252 
253      if(ans) puts("Yes");
254 
255      else puts("No");
256 
257    }
258 
259    return 0;    
260 
261  }

 关于24点的一些基于组合数学的拓展:

较有难度的24点

  虽然大多数24点存在很多解法,有相当一部分数字组合只存在唯一的解法。这种组合往往较有难度,也较为有趣。这里总结一些常见的组合。

  分数运算

  虽然给出4个数字都是整数,中间步骤中有时会出现分数。这种4个数字的组合往往较有难度。一个经典的例子是1,5,5,5,其解答为5 × (5 − 1 ÷ 5) = 24;另外 一个例子是3,3,8,8,其解答为8 ÷ (3 - 8 ÷ 3) = 24。因为后者用到了两次除法,其解法比较难以想到。另外一些类似的组合为:

数字组合

解法

数字组合

解法

 2,  4, 10, 10

(2 + 4 ÷ 10) × 10

 2,  5,  5, 10

(5 − 2 ÷ 10) × 5

 2,  7,  7, 10

(2 + 10 ÷ 7) × 7

 3,  3,  7,  7

(3 + 3 ÷ 7) × 7

 4,  4,  7,  7

(4 − 4 ÷ 7) × 7

 2,  2, 11, 11

(2 + 2 ÷ 11) × 11

 2,  2, 13, 13

(2 − 2 ÷ 13) × 13

 1,  3,  4,  6

6 ÷ (1 − 3 ÷ 4)

 2,  3,  5, 12

12 ÷ (3 − 5 ÷ 2)

 1,  8, 12, 12

12 ÷ (12 ÷ 8 - 1)

大数/奇数运算

大 多数组合中,中间步骤只会涉及到一些较小的数字(≤32)。但是有些组合中会涉及到一些较大数字,这些组合通常较有难度。比如4、4、10、10 的解法为(10 × 10 − 4) ÷ 4 = 24,5、6、6、9的解法为6 × 9 − 5 × 6 = 24。此外如果运算涉及到一些奇数的运算也会增加难度,比如6、9、9、10的解法为9 × 10 ÷ 6 + 9 = 24。一些例子如下:

数字组合

解法

数字组合

解法

 1,  3,  9, 10

(1 + 10) × 3 − 9

 7,  8,  8, 10

10 × 8 − 7 × 8

 9, 11, 12, 12

11 × 12 − 9 × 12

 1,  2,  7,  7

(7 × 7 − 1) ÷ 2

 3,  8,  8, 10

(8 × 10 − 8) ÷ 3

 4,  8,  8, 11

(8 × 11 + 8) ÷ 4

 5, 10, 10, 13

(10 × 13 − 10) ÷ 5

 1,  5, 11, 11

(11 × 11 - 1) ÷ 5

 1,  6, 11, 13

(11 × 13 + 1) ÷ 6

 1,  7, 13, 13

(13 × 13 − 1) ÷ 7

24点的组合数学

其实独立的24点的个数并不多。如果每张牌面的数值被限制在1到K之间,独立的数字组合数由有重复的组合数给出。:

    譬如,如果最大的牌面数值为10,那么独立的数字组合为715个,远比10000要小;如果最大的牌面数值为13,那么独立的数字组合为1820个。这是因为其他的组合可以通过简单的数字交换得到。

可以用枚举证明,如果最大牌面数值为10,在715个组合中,有149个组合是没有解的。此外,如果我们随机的取4个1-10之间的数字,无解的概率为1442/10000大致为1/7。如果最大牌面数值为13,则会有458个组合无解(总数为1820)。

  推广

   24点游戏可以被推广到多张牌(n>4)的情形。通常我们假定这n张牌是从一副牌中选出的,也就是说,每个数字至多出现4次。比如在5张牌、 最大数值为10的情况下,有1992种不同的组合方式。其中无解的比例大大降低,一共为如下37种。如果最大数值为13,则无解的总数扩大为80。

1 1 1 1 2

1 1 1 1 3

1 1 1 1 4

1 1 1 1 5

1 1 1 2 2

1 1 1 2 3

1 1 1 9 9

1 1 1 9 10

1 1 1 10 10

1 1 2 2 2

1 1 6 7 7

1 1 7 7 7

1 1 9 9 9

1 1 9 9 10

1 1 10 10 10

1 5 9 9 9

1 6 7 7 7

1 7 7 7 7

1 7 9 9 9

1 8 9 9 10

1 8 9 10 10

1 9 9 9 9

1 9 9 9 10

1 9 9 10 10

1 9 10 10 10

1 10 10 10 10

2 9 9 9 9

3 5 5 5 5

4 9 9 9 9

6 7 7 7 7

6 10 10 10 10

8 9 9 9 10

8 9 9 10 10

9 9 9 9 10

9 9 9 10 10

9 9 10 10 10

9 10 10 10 10

 

 

 

 

 

    注意到,以上罗列的情形中并没有5个相同数字的组合,比如,5个1,这是因为一副牌最多只有4个相同数字的牌。

   在6张牌的情况下,在4905种不同的组合方式仅有3种组合是无解的:1 1 1 1 2 2,9 9 9 10 10 10 和9 9 10 10 10 10。 在7张牌的情况下,所有组合方式(10890种)都有解。

 

posted on 2013-02-27 20:33  吴昊系列  阅读(1734)  评论(9编辑  收藏  举报

导航