吴昊品游戏核心算法 Round 12 —— 火柴游戏AI(大整数加减)

 

  如图所示,这个游戏是基于android操作系统的,名字叫《火柴棍数学》。如图所示,你移动一个火柴,就可以发现火柴上指示的公式由错误变为了正确。这是一个非常有趣的游戏,也可以锻炼自己的逻辑思维。

 

 我们这里考虑一个相对简单的情况,也就是游戏必须满足如下三个条件:

   (A)只能改变数字,不能改变符号。

 (B)数字和符号的组成方式必须严格的和图示的一样(减号由一根火柴组成)。

 (C)新等式必须形如a+b=c或a-b=c,其中a、b、c都是不含前导0的非负整数。

 最后,等式还是需要成立的。

 这是一个模拟,主要用到了大数加减的模板,解决起来还是相当棘手的,不过,分成几个模块之后,也不是非常复杂吧,我将整个标程解剖(精彩之处我都在注释中说明了):

  

  1  //头文件
  2 #include<iostream>
  3    using namespace std;
  4 
  5   开辟数据结构(主要是为每个火柴的运动所规定起始和中止的变化)
  6 
  7  char str[110], s[3][110];
  8  int cntSUB[10] = {0000001132}; //保存每个数减去一根后能能变成几个数
  9  int cntADD[10] = {1101021001}; //保存每个数加上一根后能能变成几个数
 10  int cntself[10] = {2022022002}; //保存每个数自身移动一根后能变成几个数
 11  //这里开一个二维数组的目的也是为了存储上限的情况
 12  int SUB[10][3], ADD[10][2], self[10][2], op, k, j, i, Cnt;
 13  char S[100000][103], Str[110];
 14 
 15   cmp,用于比较两个数的大小(在两数减法的时候用到)
 16 
 17   //比较两个数的大小的时候可以用到
 18  int cmp(const void *a, const void *b)
 19  {
 20    return (strcmp((char*) a, (char*) b));
 21  }
 22  大数加法的模板(进位,借位很巧妙,如果用JAVA的话就直接BigInteger了)
 23 
 24    //大数加法的模板
 25  void add(char a[], char b[], char back[])
 26  {
 27    //A+B==C?
 28    int i, j, k, up, x, y, z, l;
 29    char *c;
 30    //这里之所以加2,主要是考虑到了进位的问题
 31    if (strlen(a) > strlen(b))
 32      l = strlen(a) + 2;
 33    else l = strlen(b) + 2;
 34    //这里被动地为c开辟一个malloc空间
 35    c = (char *) malloc(l * sizeof (char));
 36    i = strlen(a) - 1;
 37    j = strlen(b) - 1;
 38    k = 0;
 39    up = 0;
 40    while (i >= 0 || j >= 0)
 41    {
 42      if (i < 0) x = '0';
 43      else x = a[i];
 44      if (j < 0) y = '0';
 45      else y = b[j];
 46      z = x - '0' + y - '0';
 47      if (up) z += 1;
 48      if (z > 9)
 49      {
 50        up = 1;
 51        z %= 10;
 52      }
 53      else up = 0;
 54      c[k++] = z + '0';
 55      i--;
 56      j--;
 57    }
 58    if (up) c[k++] = '1';
 59    i = 0;
 60    c[k] = '\0';
 61    for (k -= 1; k >= 0; k--)
 62      back[i++] = c[k];
 63    back[i] = '\0';
 64  }
 65  大数减法的模板(同理,这里用了一个loop,非主流!)
 66 
 67   //大数减法的模板
 68  void sub(char s1[], char s2[], char t[])
 69  {
 70    //A-B==C?
 71    int i, l2, l1, k;
 72    l2 = strlen(s2);
 73    l1 = strlen(s1);
 74    t[l1] = '\0';
 75    l1--;
 76    for (i = l2 - 1; i >= 0; i--, l1--)
 77    {
 78      if (s1[l1] - s2[i] >= 0)
 79        t[l1] = s1[l1] - s2[i] + '0';
 80      else
 81      {
 82        t[l1] = 10 + s1[l1] - s2[i] + '0';
 83        s1[l1 - 1] = s1[l1 - 1] - 1;
 84      }
 85    }
 86    k = l1;
 87    while (s1[k] < 0)
 88    {
 89      s1[k] += 10;
 90      s1[k - 1] -= 1;
 91      k--;
 92    }
 93    while (l1 >= 0)
 94    {
 95      t[l1] = s1[l1];
 96      l1--;
 97    }
 98    loop:
 99      if (t[0] == '0')
100      {
101        l1 = strlen(s1);
102        for (i = 0; i < l1 - 1; i++)
103          t[i] = t[i + 1];
104        t[l1 - 1] = '\0';
105        goto loop;
106      }
107    if (strlen(t) == 0)
108    {
109      t[0] = '0';
110      t[1] = '\0';
111    }
112  }
113 
114  基于前面两个模板的基础,这里可以利用check()函数来看看改变了一根火柴之后,表达式是否成立
115 
116   void check(char str[])
117  {
118    //传进一个字符串等式,判断等式是否成立
119    char re[110];
120    int l, len = strlen(str);
121    //k第k个符号,它可以是操作数,也可以是操作符,由于操作数的位数不确定,所以用j来表示操作数的位置
122    k = j = 0;
123    //首先定义一个"不可能"的操作符
124    op = -1;
125    for (i = 0; i < len; i++)
126    {
127      //从字符串中分离出各种操作数和操作符
128      if (str[i] == '+') op = 1;
129      if (str[i] == '-') op = 0;
130      if (str[i] >= '0' && str[i] <= '9')
131        s[k][j++] = str[i];
132      else
133      {
134        //遇到空格或者等号的时候,我们将一个操作数/操作符存入一个二维数组的一行,并开启新的一行
135        s[k][j] = '\0';
136        //我们准备为新的一个表达式铺路,并且把j重新清理为0
137        k++;
138        j = 0;
139      }
140    }
141    //为表达式的最后一个操作数/操作符配上\0
142    s[k][j] = '\0';
143    //我们的二维数组总共存储了三个数(这三个数也许是很大的,所以需要大数加减法)
144    for (i = 0; i < 3; i++)
145    {
146      j = 0;
147      if (s[i][j] != '0'continue;
148      while (s[i][j])
149      {
150        if (s[i][j] != '0'break;
151        j++;
152      }
153      //这里花了那么大的劲主要是看有没有拆成这个数的所有位数全是0的情况,这样也是不符合要求的
154      if (s[i][j]) return;
155    }
156    int len0 = strlen(s[0]);
157    int len1 = strlen(s[1]);
158    //如果符号为加号的话,尝试大数加法
159    if (op == 1)
160    {
161      add(s[0], s[1], re);
162      //得到的结果如果和s[2]也就是等号右边的数字完全一样的话,可以得到一个符合条件的答案
163      if (strcmp(re, s[2]) == 0)
164        strcpy(S[Cnt++], str);
165    }
166    else
167    {
168      //如果被减数比减数小的话,也是不可能的,因为这里都是非负整数,所以退出
169      if (len0 < len1) return;
170      //这里考虑如果减数和被减数的位数相等的情况
171      else if (len0 == len1)
172      {
173        for (l = 0; l < len1; l++)
174        {
175          //找到一个位上的数字不相等的位置(这里本质上也是比较两个数的大小)
176          if (s[0][l] != s[1][l])
177            break;
178        }
179        //经过一系列的判断,如果被减数比减数小的话,也选择退出
180        if (l != len1 && s[0][l] < s[1][l])
181          return;
182      }
183      //同理,做减法
184      sub(s[0], s[1], re);
185      if (strcmp(re, s[2]) == 0)
186        strcpy(S[Cnt++], str);
187    }
188    //这里不明白是在做什么,排除重复的情况么?置疑
189    if (strcmp(Str, S[Cnt - 1]) == 0)
190      Cnt--;
191  }
192 
193  加上一个火柴,对整个表达式的变化(由于减少一根火柴的同时,会在另外一边多加一个火柴(这里不考虑自加的情况),那么,加和减实际上可以同时考虑,这里是先减后加再check())
194 
195  void addfun(int dex, char a[])
196  {
197    // 枚举每个加上一根
198    int i, j, t, len = strlen(a);
199    for (i = 0; i < len; i++)
200    {
201      if (a[i] == '=' || a[i] == '-' || a[i] == '+'continue;
202      t = a[i] - '0';
203      //如果满足如下两个条件,就是(1)要摆弄的火柴不在自己本身的位置(2)存在有可以通过一个火柴变为一个新数的火柴数
204      if (i != dex && cntADD[t])
205      {
206        for (j = 0; j < cntADD[t]; j++)
207        {
208          a[i] = ADD[t][j] + '0';
209          //检查一下这个表达式在改变之后是否符合要求
210          check(a);
211        }
212        a[i] = t + '0';
213      }
214    }
215  }
216 
217  同上,剪掉一根火柴之后,整个表达式的变化
218 
219    void subfun(char *a)
220  {
221    // 枚举每个减去一根
222    int i, j, t, len = strlen(a);
223    for (i = 0; i < len; i++)
224    {
225      //假设有空格,也没有什么影响
226      if (a[i] == '=' || a[i] == '-' || a[i] == '+'continue;
227      t = a[i] - '0';
228      if (cntSUB[t])
229      {
230        for (j = 0; j < cntSUB[t]; j++)
231        {
232          //剪掉一根火柴,还需要在另外一个数上安放一个火柴
233          a[i] = SUB[t][j] + '0';
234          //不允许再在i这个位置上摆弄火柴了
235          addfun(i, a);
236        }
237        a[i] = t + '0';
238      }
239    }
240  }
241 
242  我们考虑如果是自身的移动的话,会发生什么情况(这应该属于第二大类型吧)
243   void selffun(char a[])
244  {
245    // 枚举每个自身移动
246    int i, j, t, len = strlen(a);
247    for (i = 0; i < len; i++)
248    {
249      //这里仅仅需要读取数字,不理睬运算符
250      if (!(str[i] <= '9' && str[i] >= '0')) continue;
251      t = a[i] - '0';
252      if (cntself[t])
253      {
254        for (j = 0; j < cntself[t]; j++)
255        {
256          //剪掉一根火柴,还需要在另外一个数上安放一个火柴 
257          a[i] = self[t][j] + '0';
258          check(a);
259        }
260      }
261      else
262        continue;
263      a[i] = t + '0';
264    }
265  }
266 
267  最后是主函数,初始化各种数值,将输入输出配置好,整个游戏就GAME OVER了!(这里是要求排序输出的,所以qsort一下!)
268  int main()
269  {
270    //将所有增加一根火柴后可以变化成新的数字的情况以及变化成的新的数字予以罗列
271    ADD[0][0] = 8;
272    ADD[1][0] = 7;
273    ADD[3][0] = 9;
274    ADD[5][0] = 6;
275    ADD[5][1] = 9;
276    ADD[6][0] = 8;
277    ADD[9][0] = 8;
278    SUB[6][0] = 5;
279    //将所有减少一根火柴后可以变化成新的数字的情况以及变化成的新的数字予以罗列
280    SUB[7][0] = 1;
281    SUB[8][0] = 0;
282    SUB[8][1] = 6;
283    SUB[8][2] = 9;
284    SUB[9][0] = 3;
285    SUB[9][1] = 5;
286    //将自身移动一根火柴后可以变化成新的数字的情况以及变化成的新的数字予以罗列
287    self[0][0] = 6;
288    self[0][1] = 9;
289    self[2][0] = 3;
290    self[3][0] = 2;
291    self[3][1] = 5;
292    self[5][0] = 3;
293    self[6][0] = 0;
294    self[6][1] = 9;
295    self[9][0] = 0;
296    self[9][1] = 6;
297    //这里直到读到文件的末尾位置
298    while (scanf("%s", str) == 1)
299    {
300      Cnt = 0;
301      //将原来的表达式字符串COPY到Str中
302      strcpy(Str, str);
303      subfun(str);
304      selffun(str);
305      //无解的情况
306      if (!Cnt)
307      {
308        puts("-1");
309        continue;
310      }
311      qsort(S, Cnt, sizeof (S[0]), cmp);
312      puts(S[0]);
313      for (int i = 1; i < Cnt; i++)
314 
315      //对于相等的情况,是不可以重复输出的
316        if (strcmp(S[i], S[i - 1]))
317          puts(S[i]);
318    }
319    return 0;
320  }

 

 另外,一位叫Yu-Kenneth的大神提供了一种新颖的方法,学电信的同学肯定是熟悉的,就是七位译码表法:

 相应的译码表为:

如果用char类型的高7位来保存火柴编码的信息,最低一位一律为0,那么火柴拼成数字的码表为:

static const char NUM_CODE[10] = {0xFC, 0×60, 0xDA, 0xF2, 0×66, 0xB6, 0xBE, 0xE0, 0xFE, 0xF6};

posted on 2013-02-28 13:42  吴昊系列  阅读(370)  评论(0编辑  收藏  举报

导航