吴昊品游戏核心算法 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] = {0, 0, 0, 0, 0, 0, 1, 1, 3, 2}; //保存每个数减去一根后能能变成几个数
9 int cntADD[10] = {1, 1, 0, 1, 0, 2, 1, 0, 0, 1}; //保存每个数加上一根后能能变成几个数
10 int cntself[10] = {2, 0, 2, 2, 0, 2, 2, 0, 0, 2}; //保存每个数自身移动一根后能变成几个数
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 }
2 #include<iostream>
3 using namespace std;
4
5 开辟数据结构(主要是为每个火柴的运动所规定起始和中止的变化)
6
7 char str[110], s[3][110];
8 int cntSUB[10] = {0, 0, 0, 0, 0, 0, 1, 1, 3, 2}; //保存每个数减去一根后能能变成几个数
9 int cntADD[10] = {1, 1, 0, 1, 0, 2, 1, 0, 0, 1}; //保存每个数加上一根后能能变成几个数
10 int cntself[10] = {2, 0, 2, 2, 0, 2, 2, 0, 0, 2}; //保存每个数自身移动一根后能变成几个数
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};