用广度优先搜索解一个NOI迷宫问题
这个问题我做了不少尝试,而最后使用的方法是BFS。开始尝试了带有启发和剪枝的DFS,带有重复路径检测贪心等一些思路,但是都超时了。思考了一下,主要问题在于这些算法对没有任何阻挡(或极少阻挡)的情况不够适应,当然,进一步改进采用多种算法结合的方式还是可以解决问题的,而后测试了B*的思路,能够完成题目。最后使用了BFS,也很容易就过关了。这个题目是这样的:
2727:仙岛求药 查看 提交 统计 提问 总时间限制: 1000ms 内存限制: 65536kB 描述 少年李逍遥的婶婶病了,王小虎介绍他去一趟仙灵岛,向仙女姐姐要仙丹救婶婶。叛逆但孝顺的李逍遥闯进了仙灵岛,克服了千险万难来到岛的中心,发现仙药摆在了迷阵的深处。迷阵由M×N个方格组成,有的方格内有可以瞬秒李逍遥的怪物,而有的方格内则是安全。现在李逍遥想尽快找到仙药,显然他应避开有怪物的方格,并经过最少的方格,而且那里会有神秘人物等待着他。现在要求你来帮助他实现这个目标。 下图 显示了一个迷阵的样例及李逍遥找到仙药的路线. 输入 输入有多组测试数据. 每组测试数据以两个非零整数 M 和 N 开始,两者均不大于20。M 表示迷阵行数, N 表示迷阵列数。接下来有 M 行, 每行包含N个字符,不同字符分别代表不同含义: 1) ‘@’:少年李逍遥所在的位置; 2) ‘.’:可以安全通行的方格; 3) ‘#’:有怪物的方格; 4) ‘*’:仙药所在位置。 当在一行中读入的是两个零时,表示输入结束。 输出 对于每组测试数据,分别输出一行,该行包含李逍遥找到仙药需要穿过的最少的方格数目(计数包括初始位置的方块)。如果他不可能找到仙药, 则输出 -1。 样例输入 8 8 .@##...# #....#.# #.#.##.. ..#.###. #.#...#. ..###.#. ...#.*.. .#...### 6 5 .*.#. .#... ..##. ..... .#... ....@ 9 6 .#..#. .#.*.# .####. ..#... ..#... ..#... ..#... #.@.## .#..#. 0 0 样例输出 10 8 -1
用图片更简洁一些:
这是一个比较“标准”但非常简单的迷宫,而测试数据明显包含只有少量的阻挡甚至没有阻挡的情况。不谈这个问题,先看一下广度优先搜索和深度优先搜索的区别:
个人觉得,其最大差异就在于后进先出还是先进先出,即使用stack还是queue,后进先出导致按深度优先遍历,而先进先出导致按广度优先遍历。于是,它们的代码框架都一样:
1、第一个元素入栈或入队
2、while至为栈或队列为空
3、循环中出栈或出队,判断是否达到终点,达到则退出;不达到则将相关点进行入栈或入队
所以这份代码还是很好写的,毕竟之前用深度优先写了若干次棋类引擎:
bool BFS(){ int cnt,i; point cp,step[4]; queue<point> q; q.push(sp); bitset(tab,sp.x,sp.y,false); while(!q.empty()){ cp=q.front(); q.pop(); cnt=NextStep(step,cp); for(i=0;i<cnt;i++){ if(step[i].x==dp.x && step[i].y==dp.y){ return true; } q.push(step[i]); bitset(tab,step[i].x,step[i].y,false); } } return false; }
这就是核心算法了。之前为了加速运行,写了一个微型的bit操作:
void bitset(int arr[],int x,int y,bool val){ if(val){ arr[y]=arr[y] | (1<<x); }else{ arr[y]=arr[y] & (~(1<<x)); } } bool bitget(int arr[],int x,int y){ return ((arr[y]>>x) & 1)==1; }
实际上应该是有一定提升的,因为NOI对C++编译使用了O2开关。而相关的走法生成器是这样的:
int NextStep(point step[],point curpoint){ int cnt=0,x=curpoint.x,y=curpoint.y; if(bitget(tab,x+1,y)){ step[cnt].x=x+1;step[cnt].y=y; cnt++; } if(x>0 && bitget(tab,x-1,y)){ step[cnt].x=x-1;step[cnt].y=y; cnt++; } if(bitget(tab,x,y+1)){ step[cnt].x=x;step[cnt].y=y+1; cnt++; } if(y>0 && bitget(tab,x,y-1)){ step[cnt].x=x;step[cnt].y=y-1; cnt++; } return cnt; }
因为题目中最多用到20位,而我在初始化时设置了tab[21]所以不用担心下标超出上界。其实代码中也可以不判断下界,只需要把map映射到tab中间,在其外围留下空行。
仔细观察BFS部分的代码,发现我们的代码只能返回可否从起点达到终点,这显然是一个阉版,为了返回路径长度,我们需要一个其他的变量来进行记录,方式有多种,一般来说编码时会考虑可以返回路径,所以我们用一个二维数组来记录父节点,数组元素下标就是当前位置(step[i]),而值指向前一步(树中的父节点)的坐标(cp),这很容易实现:
path[step[i].y][step[i].x]=cp;
这样,我们就可以在BFS返回true时,在path数组中得到路径:
point cp=dp;
while(cp.x!=-1 && cp.y!=-1){ cp=path[cp.y][cp.x]; cout<<cp.x<<" "<<cp.y<<" "; }
那么题目中的计数也就很容易解决了。当然,如果只进行计数而不返回路径,可以采用其他的方式(例如在point结构中增加一个变量)。然后就是完整的代码:
#include<iostream> #include<queue> #include<cstring> using namespace std; struct point{ int x; int y; }; int pointsize=sizeof(point); int map[20]; int tab[21]; int m,n; point sp,dp,path[20][20]; int getpath(){ int pathpointcnt=0; point cp=dp; while(cp.x!=-1 && cp.y!=-1){ cp=path[cp.y][cp.x]; pathpointcnt++; } return pathpointcnt-1; } void bitset(int arr[],int x,int y,bool val){ if(val){ arr[y]=arr[y] | (1<<x); }else{ arr[y]=arr[y] & (~(1<<x)); } } bool bitget(int arr[],int x,int y){ return ((arr[y]>>x) & 1)==1; } int NextStep(point step[],point curpoint){ int cnt=0,x=curpoint.x,y=curpoint.y; if(bitget(tab,x+1,y)){ step[cnt].x=x+1;step[cnt].y=y; cnt++; } if(x>0 && bitget(tab,x-1,y)){ step[cnt].x=x-1;step[cnt].y=y; cnt++; } if(bitget(tab,x,y+1)){ step[cnt].x=x;step[cnt].y=y+1; cnt++; } if(y>0 && bitget(tab,x,y-1)){ step[cnt].x=x;step[cnt].y=y-1; cnt++; } return cnt; } bool BFS(){ int cnt,i; point cp,step[4]; queue<point> q; q.push(sp); path[sp.y][sp.x].x=-1; path[sp.y][sp.x].y=-1; bitset(tab,sp.x,sp.y,false); while(!q.empty()){ cp=q.front(); q.pop(); cnt=NextStep(step,cp); for(i=0;i<cnt;i++){ path[step[i].y][step[i].x]=cp; if(step[i].x==dp.x && step[i].y==dp.y){ return true; } q.push(step[i]); bitset(tab,step[i].x,step[i].y,false); } } return false; } int main(){ int x,y; string s; while(true){ memset(tab,0,sizeof(tab)); memset(map,0,sizeof(map)); cin>>m>>n; if(m==0 || n==0){ break; } for(y=0;y<m;y++){ cin>>s; for(x=0;x<n;x++){ if(s[x]=='@'){ sp.x=x;sp.y=y; }else if(s[x]=='*'){ dp.x=x;dp.y=y; } bitset(map,x,y,s[x]!='#'); bitset(tab,x,y,s[x]!='#'); } } if(BFS()){ cout<<getpath()<<endl; }else{ cout<<-1<<endl; } } }
类似这个问题还有一个“农夫抓牛”的问题。提交了好几次,唉。这个问题是这样的:只能对n进行+1,-1,*2操作,最少操作多少次能达到k;其中n,k均属于区间[0,100000]。这个问题实际上要比迷宫的实现起来简单一些,但是开始写的时候分心了,把path定义成[100000]囧啊。这个问题的分析类似于上面的迷宫,迷宫那个每个节点都有4个子节点,这个每个节点都有3个子节点(当然有些节点必须剪枝,否则就可能陷入死循环);但是这个是一个一维问题,所以记录表就直接用一维就可以了。另外,题目只提供了一种沿着数轴往左走的方法,所以一旦初始k<=n那么就无需搜索了,直接返回n-k即可:
#include<iostream> #include<queue> #include<cstring> using namespace std; const int minval=0,maxval=100000; int BFS(int n,int k){ int cur,next,path[maxval+1]; memset(path,-1,sizeof(path)); queue<int> q; q.push(n); path[n]=n; while(!q.empty()){ cur=q.front(); q.pop(); if(cur==k){ k=0; while(cur!=n){ cur=path[cur]; k++; } return k; } next=cur-1; if(next>=minval && path[next]==-1){ q.push(next); path[next]=cur; } next=cur+1; if(next<=maxval && path[next]==-1){ q.push(next); path[next]=cur; } next=cur*2; if(next<=maxval && path[next]==-1){ q.push(next); path[next]=cur; } } return -1; } int main(){ int n,k; cin>>n>>k; if(n>=k){ cout<<(n-k); }else{ cout<<BFS(n,k)<<endl; } }
当然,这里也可以把path[next]另用:path[next]=path[cur]+1;就是说用作已访问位置标志表的同时直接记录歩数而不是记录上一步的位置。但是如果这样做要得到路径时就有困难了。
最后,补充一下为什么广度优先搜索得到的第一个解就是最短路径,非常好理解:把起点看作根节点构建一个树,那么广度优先搜索是按深度逐步加深的——一层一层的搜索,同一层上的深度一致,而这两个问题边的权值都是1,所以第一个解一定是最短路径。
添加一个内容,从昨天思考这个鸣人和佐助的问题。这个问题本质也是一个迷宫,只是这个迷宫的墙可以打破,每打破一个墙需要一点体力,问在一个M*N的迷宫中体力为T时到达终点的最短路径。这个问题看起来貌似不难,但其实还是需要多思考一下:
1、不是走过的路径更短就意味着更好,后面没有体力了也可能遇到绕过去更费力的墙;
2、也不是走过的路径越长越好(剩下更多的体力后面可能没有墙穿或者穿了也没有现在用掉减少的路径长度多)。
其中第一点导致了可以重复搜索走过的路径。那么问题就来了,我们知道如果把走过的路径再次入队可能会导致程序陷入死循环,所以不能无条件的入队,于是入队时要进行限制。可能会想到体力多点更好,但事实还有第二点的约束,所以老老实实的限定不能以相同体力走过相同点来避免陷入循环即可,否则就画蛇添足了。
那么代码实现起来就会比之前的稍微麻烦一点,需要给path填上第三维或者在point里面加一个数组来标志是否以某个体力走过,根据题意,体力小于10。先附上两组测试数据:
7 7 1 ##*@*## ##*#*## ##***## ###*### #*****# #*###*# #**+**# 7 7 1 #**@**# #*###*# #*****# ###*### ##***## ##*#*## ##*+*##
这两组数据就是刚才提到的那个关键问题:什么时候拿体力换路径缩短更划算的问题。另外,注意题目要求无法达到终点时输出-1,这肯定也是一个测试点咯。
最后,附上代码:
#include<iostream> #include<cstring> #include<queue> using namespace std; struct point{ int x; int y; int t; }; int m,n,map[201][201]; point sp,dp,path[201][201][10],dir[4]={{0,1},{1,0},{0,-1},{-1,0}}; int pathlen(point p){ int cnt=0; point cp=p; while(cp.x!=sp.x || cp.y!=sp.y){ cp=path[cp.y][cp.x][cp.t]; cnt++; } return cnt; } int nextstep(point cp,point step[]){ int cnt=0; point tp; for(int i=0;i<4;i++){ tp.x=cp.x+dir[i].x; tp.y=cp.y+dir[i].y; if(0<=tp.x && tp.x<n && 0<=tp.y && tp.y<m){ tp.t=cp.t+map[tp.y][tp.x]; if(tp.t>=0 && path[tp.y][tp.x][tp.t].t==-1){ step[cnt++]=tp; } } } return cnt; } int BFS(){ queue<point> q; q.push(sp); path[sp.y][sp.x][sp.t]=sp; while(!q.empty()){ if(q.front().x==dp.x && q.front().y==dp.y){ return pathlen(dp); }else{ point step[4]; int stepcnt=nextstep(q.front(),step); for(int i=0;i<stepcnt;i++){ q.push(step[i]); path[step[i].y][step[i].x][step[i].t]=q.front(); } } q.pop(); } return -1; } int main(){ cin>>m>>n>>sp.t; string line; memset(path,-1,sizeof(path)); for(int y=0;y<m;y++){ cin>>line; for(int x=0;x<n;x++){ if(line[x]=='@'){ sp.x=x;sp.y=y; } if(line[x]=='+'){ dp.x=x;dp.y=y; } if(line[x]=='#'){ map[y][x]=-1; }else{ map[y][x]=0; } } } cout<<BFS(); }