如果你认为无法因为玩一个电脑游戏而达到精神的顿悟,你可能是正确的。不过你完全可以试着解决一个禅宗花园发生的难题,从而达到静心的精神状态。
这个游戏是获得2003年独立游戏节提名的精品游戏,在注重游戏画面和特效的今天,很多人无法接触到和了解这个游戏深刻内涵,特别推荐小游戏玩家来挑战这个禅宗花园的难题。
如 图,这就是那个2003年的电脑游戏的截图,其挂卡的设计我在具体的AI实现中会说到,其实该游戏的关卡已经被证明是NP完全的(我在Round 14中也阐述过类似的关卡设计问题,也就是推箱子的关卡设计,一个有挑战的推箱子关卡往往被设计成指数级别的复杂度)。由于当时的智能手机并没有发达到如 今的地步,所以该游戏最初是在电脑端进行的单机游戏。
如今,该游戏已经被安置到手机游戏的银屏上,比如这款基于ipad平台开发的小游戏,大有昔日的魂斗罗重登PS2之感啊!
这里,注意到下角有一些辅助工具,这些都是一些AI小应用,自动退回,自动步进,以及让“智慧老人”指引你找到一条路径等等。可以看到,在手机端的改进版中,形状已经不是规则的矩形了,这也给AI的设计者提出了更大的挑战。
关于游戏挂卡的NP完全证明,我在道客巴巴找到了一篇还没有被翻译出来的英文文献,我会和同学一起翻译,并在这一期Round给出。
游戏的规则
我 这里截取费恩曼在他的《费恩曼物理学讲义》中的一段话:研究物理学就好比研究两个绝世高手下棋,往往先要破解这个游戏的规则,这其实比较容易做到,但是, 我们在这个基础之上,还要想一想,他们是为什么要那么下棋?这就是要破解他们基于这一规则所制定的策略,这往往就是难上加难了,我们可以理解为模拟算法和 AI算法的区别。而且,往往最简单的规则的游戏会蕴含着最深奥的策略,比如围棋。
如图(a),(b),(c),一个分块的矩形中间夹着几个石头,一个小人在上面行走。准确地说,应该是滑行吧,当他碰到石头的时候,就可以考虑换一个方向 进行滑动。每次滑行道离开这个沙场位置。我们最终的目标(goal)是将整个没有石头的沙子都恰好走过一遍(这里的恰好走过一遍的意思是:经过的沙地就不 允许再经过了),而且,这个小人在执行了这个过程之后,最终应该出现在沙子的外面。
(如图,这是Zen Puzzle Garden目前的官方网站,为一个经典的游戏设置一个官网也是必须的事情)
我们设计一款AI,可以再20s之内至少返回一个合理解(无解的情况暂时不考虑),将游戏的界面大小设计为12*12的模式,输入为一个界面,其中0来标记空地,1标记石子。输出的第一行为需要行走的轮数,后面为每一轮的具体步骤。
该问题可以考虑为“吴昊系列Round 16——龙系道馆”的延伸,我们的主角还是采用“滑动”的模式,只是这一次不同之处在于,每一个格子走过之后就不允许再走了,所以,需要用一个visit 数组进行标记。而且,最后的游戏目标是恰好一次扫描到最有的格子,而不是到达一个目的地,所以,异常麻烦,源码我用的是Pengjiajun(NOI) 的,这里注明一下,有些地方还是木有看懂,他使用了三个函数,进行如下的调用:
bfs()判断每一块小方形所能延拓的空地的总数(除开那个小方块本身)。
bool dfs(int a[],pp nw,int ndeep)判断这个游戏是否存在一个解,如果搜索到了,则输出第一个搜索到的解。
bool t_dfs(int a[],int id1,int id2,int na[],int dir,pp now,int deep,int ndeep)以一个固定的点,固定的步进度进行深度优先搜索,这是一个递归的函数,直到找到一个满足条件的解,该函数是dfs函数的子函数。
另外,设置了数组a[],利用30进制进行记录,应该是对空间的一个优化吧,暂时还木有搞得很明白。
2 #include<cstdlib>
3 #include<algorithm>
4 #include<cmath>
5 #include<cstring>
6 #incude<stdio.h>
7 #include<map>
8 #include<vector>
9 using namespace std;
10
11 vector< int> adj[ 6][ 999997];
12
13 int r,c,cnt;
14 int MOD= 999997;
15
16 // 这里定义方向向量,便于搜索
17 int dirx[]={- 1, 1, 0, 0};
18 int diry[]={ 0, 0,- 1, 1};
19
20 // 定义一个结构体,存储地图
21 struct pp
22 {
23 bool mat[ 20][ 20];
24 };
25
26 struct bl
27 {
28 int num;
29 short list[ 145][ 2];
30 bool operator <( const bl &temp) const
31 {
32 return num<temp.num;
33 }
34 };
35
36 pp nw;
37
38 struct qq
39 {
40 int id1,id2;
41 };
42
43 qq list[ 200],queue[ 200];
44
45 // 这里标记是否经历过
46 bool visit[ 13][ 13];
47
48 int f[ 20][ 20],temp1,temp2,temp,ans[ 200][ 200][ 2],num[ 200],cont;
49
50 bool t_dfs( int a[], int id1, int id2, int na[], int dir,pp now, int deep, int ndeep);
51 bool dfs( int a[],pp nw, int ndeep);
52
53 void bfs( int id1, int id2,pp nw)
54 {
55 int i,j,s,p,q;
56 // 将这个空白方块入队列,并标记为已经访问
57 queue[ 0].id1=id1;
58 queue[ 0].id2=id2;
59 visit[id1][id2]= true;
60 temp1=temp2= 0;
61 temp= 1;
62 while(temp1<=temp2)
63 {
64 for(i=temp1;i<=temp2;i++)
65 {
66 // 分别对四个方向进行BFS
67 for(j= 0;j< 4;j++)
68 {
69 id1=queue[i].id1+dirx[j];
70 id2=queue[i].id2+diry[j];
71 // 判断搜索的点是否越界
72 if(id1>= 0&&id1<r&&id2>= 0&&id2<c)
73 {
74 // 如果延拓的这一点是空地而且未被访问的话
75 if(visit[id1][id2]== false&&nw.mat[id1][id2]== 0)
76 {
77 // 标记为已访问,并且入队列
78 visit[id1][id2]= true;
79 queue[temp].id1=id1;
80 queue[temp++].id2=id2;
81 }
82 }
83 }
84 }
85 // 这里的含义是,去掉一个已经出队的点,增加新入队的点
86 temp1=temp2+ 1;
87 temp2=temp- 1;
88 }
89 }
90
91 int main()
92 {
93 int i,j,ncnt,a[ 5];
94 scanf( " %d%d ",&r,&c);
95 cnt= 0;
96 memset(f,- 1, sizeof(f));
97 for(i= 0;i<r;i++)
98 {
99 for(j= 0;j<c;j++)
100 {
101 scanf( " %d ",&nw.mat[i][j]);
102 if(nw.mat[i][j]== 0)
103 {
104 list[cnt].id1=i;
105 list[cnt].id2=j;
106 f[i][j]=cnt++;
107 }
108 }
109 }
110 memset(num, 0, sizeof(num));
111 cont= 0;
112 // orz为真值,看是为true还是false
113 int orz=dfs(a,nw, 0);
114 if(orz> 0)
115 {
116 // 总共需要行进几轮
117 printf( " %d\n ",cont);
118 for(i= 0;i<cont;i++)
119 {
120 // 每一轮的行进步数
121 printf( " %d: ",num[i]);
122 // 加1的原因是,那个数组是从0下标开始计数的
123 for(j= 0;j<num[i];j++)
124 printf( " (%d,%d) ",ans[i][j][ 0]+ 1,ans[i][j][ 1]+ 1);
125 printf( " \n ");
126 }
127 }
128 return 0;
129 }
130
131 bool t_dfs( int a[], int id1, int id2, int na[], int dir,pp now, int deep, int ndeep)
132 {
133 int i,j,x,y,id,b[ 6];
134 ans[ndeep][deep][ 0]=id1;
135 ans[ndeep][deep][ 1]=id2;
136 // 将目前的点标记为石头,表明已经走过
137 now.mat[id1][id2]= 1;
138 // 对已经选定好的方向进行搜索
139 x=id1+dirx[dir];
140 y=id2+diry[dir];
141 if(x< 0||x>=r||y< 0||y>=c)
142 {
143 // 如果已经出界,记录答案
144 ans[ndeep][deep+ 1][ 0]=x;
145 ans[ndeep][deep+ 1][ 1]=y;
146 num[ndeep]=deep+ 2;
147 if(num[ndeep]> 3)
148 {
149 if(dfs(b,now,ndeep+ 1))
150 return true;
151 }
152 // 不行的话,重新标记为空地
153 now.mat[id1][id2]= 0;
154 return false;
155 }
156 // 如果没有越界而且这里为空地的话,则可能满足题目条件
157 if(now.mat[x][y]== 0)
158 {
159 // 搜索深度每次加深1个单位
160 if(t_dfs(a,x,y,na,dir,now,deep+ 1,ndeep)) return true;
161 // 否则,退回到原来的状态
162 now.mat[id1][id2]= 0;
163 return false;
164 }
165 // 如果不是空地的话,朝四个方向搜索
166 else
167 {
168 for(i= 0;i< 4;i++)
169 {
170 x=id1+dirx[i];
171 y=id2+diry[i];
172 if(x< 0||x>=r||y< 0||y>=c)
173 {
174 ans[ndeep][deep+ 1][ 0]=x;
175 ans[ndeep][deep+ 1][ 1]=y;
176 if(num[ndeep]> 3)
177 {
178 if(dfs(b,now,ndeep+ 1)) return true;
179 }
180 }
181 else if(now.mat[x][y]== 0)
182 {
183 if(t_dfs(a,x,y,na,i,now,deep+ 1,ndeep)) return true;
184 }
185 }
186 // 否则,将其重新标为空地,返回false
187 now.mat[id1][id2]= 0;
188 return false;
189 }
190 }
191
192 bool dfs( int a[],pp nw, int ndeep)
193 {
194 int siz,value= 0,i,j,s= 0,cou= 0,na[ 5],b[ 6],id, in= 1000000000;
195 pp now;
196 bl block[ 20];
197 a[ 0]=a[ 1]=a[ 2]=a[ 3]=a[ 4]= 0;
198 // 利用一个数组模拟30进制存储
199 for(i= 0;i<r;i++)
200 {
201 for(j= 0;j<c;j++)
202 {
203 if(nw.mat[i][j]== 0)
204 {
205 int id=f[i][j];
206 a[id/ 30]+=( 1<<(id% 30));
207 }
208 }
209 }
210 if(a[ 0]== 0&&a[ 1]== 0&&a[ 2]== 0&&a[ 3]== 0&&a[ 4]== 0)
211 {
212 cont=ndeep;
213 return true;
214 }
215 // 否则,按照键值存储一个状态
216 for(i= 4;i>= 0;i--)
217 value=((( long long)(( 1<< 30)%MOD)*( long long)value)%MOD+a[i])%MOD;
218 siz=adj[ 0][value].size();
219 // 这是判断是否越出了五位的界么?这个逻辑实现的功能还没怎么看懂
220 for(i= 0;i<siz;i++)
221 {
222 for(j= 0;j< 5;j++)
223 {
224 if(adj[j][value][i]!=a[j]) break;
225 }
226 if(j>= 5) break;
227 }
228 if(i<siz) return adj[ 5][value][i];
229 memset(visit, false, sizeof(visit));
230 for(i= 0;i<r;i++)
231 for(j= 0;j<c;j++)
232 {
233 // 对于每一个还没有遍历,且为空地的地方,进行bfs扫描
234 if(visit[i][j]== false&&nw.mat[i][j]== 0)
235 {
236 bfs(i,j,nw);
237 // 每次新增的块
238 block[cou].num=temp;
239 // 将对每一点搜索的新增的块都加入到list中
240 for(s= 0;s<temp;s++)
241 {
242 block[cou].list[s][ 0]=queue[s].id1;
243 block[cou].list[s][ 1]=queue[s].id2;
244 }
245 cou++;
246 }
247 }
248 // 对每一个块进行分析
249 for(i= 0;i<cou;i++)
250 {
251 // 利用变量orz存储由这个方块延拓的遇到边界的次数(神牛就是神牛,变量名都那么精彩)
252 int orz= 0;
253 // 对那一块所有新增加的块进行分析
254 for(j= 0;j<block[i].num;j++)
255 {
256 if(block[i].list[j][ 0]== 0)
257 orz++;
258 else if(block[i].list[j][ 0]==r- 1)
259 orz++;
260 else if(block[i].list[j][ 1]== 0)
261 orz++;
262 else if(block[i].list[j][ 1]==c- 1)
263 orz++;
264 }
265 // 得到块数最小的点
266 if( in>block[i].num)
267 {
268 in=block[i].num;
269 id=i;
270 }
271 // 如果只能触及到一个边界,那么是不行的
272 if(orz<= 1) return false;
273 }
274 }
275 // 这里给出了我们的AI策略,每次选取延拓方块最小的,这样最不容易导致出现"石头阻挡"或者"经历过的点,绕不回来"的毛病
276 swap(block[id],block[ 0]);
277 for(i= 0;i<block[ 0].num;i++)
278 {
279 // 对块延拓出的边界点进行处理,广度搜索,并存储在a[]中
280 if(block[ 0].list[i][ 0]== 0)
281 {
282 memset(na, 0, sizeof(na));
283 now=nw;
284 num[ndeep]= 0;
285 ans[ndeep][num[ndeep]][ 0]=- 1;
286 ans[ndeep][num[ndeep]++][ 1]=block[ 0].list[i][ 1];
287 // 如果这条路径可达的话
288 if(t_dfs(a,block[ 0].list[i][ 0],block[ 0].list[i][ 1],na, 1,now, 1,ndeep))
289 {
290 for(j= 0;j< 5;j++)
291 adj[j][value].push_back(a[j]);
292 adj[ 5][value].push_back( 1);
293 return true;
294 }
295 }
296 if(block[ 0].list[i][ 0]==r- 1)
297 {
298 memset(na, 0, sizeof(na));
299 now=nw;
300 num[ndeep]= 0;
301 ans[ndeep][num[ndeep]][ 0]=r;
302 ans[ndeep][num[ndeep]++][ 1]=block[ 0].list[i][ 1];
303 if(t_dfs(a,block[ 0].list[i][ 0],block[ 0].list[i][ 1],na, 0,now, 1,ndeep))
304 {
305 for(j= 0;j< 5;j++)
306 adj[j][value].push_back(a[j]);
307 adj[ 5][value].push_back( 1);
308 return true;
309 }
310 }
311 if(block[ 0].list[i][ 1]== 0)
312 {
313 memset(na, 0, sizeof(na));
314 now=nw;
315 num[ndeep]= 0;
316 ans[ndeep][num[ndeep]][ 0]=block[ 0].list[i][ 0];
317 ans[ndeep][num[ndeep]++][ 1]=- 1;
318 if(t_dfs(a,block[ 0].list[i][ 0],block[ 0].list[i][ 1],na, 3,now, 1,ndeep))
319 {
320 for(j= 0;j< 5;j++)
321 adj[j][value].push_back(a[j]);
322 adj[ 5][value].push_back( 1);
323 return true;
324 }
325 }
326 if(block[ 0].list[i][ 1]==c- 1)
327 {
328 memset(na, 0, sizeof(na));
329 now=nw;
330 num[ndeep]= 0;
331 ans[ndeep][num[ndeep]][ 0]=block[ 0].list[i][ 0];
332 ans[ndeep][num[ndeep]++][ 1]=c;
333 if(t_dfs(a,block[ 0].list[i][ 0],block[ 0].list[i][ 1],na, 2,now, 1,ndeep))
334 {
335 for(j= 0;j< 5;j++)
336 adj[j][value].push_back(a[j]);
337 adj[ 5][value].push_back( 1);
338 return true;
339 }
340 }
341 }
342 for(j= 0;j< 5;j++)
343 adj[j][value].push_back(a[j]);
344 adj[ 5][value].push_back( 0);
345 return false;
346 }
347
348
349
350
351
352