对此题有疑问的小伙伴们,相信你们之前肯定对BFS广度优先搜索算法有所了解,也做过一些较为基础的例题,最经典的问题之一应该是迷宫问题,从起点S出发,求到达终点E的最短路径,迷宫问题的解决思路是利用BFS从起点S出发,一层一层往外进行地毯式的搜索,直至到达终点E,即可求出最短路径。可以看出,在迷宫问题中,我们只需要应用一次BFS算法,即可求出最短路径。因为起点S和终点E的坐标都只有一个,即有唯一的起点坐标和终点坐标。
解题思路:
首先来分析一下问题, 这道题最关键的一个地方就是钥匙被放置在多处,蒜头君有多种选择,那怎么知道哪一条路径(起点->钥匙位置->家)是最短的呢?
在这里,我就以一个简单地图的例子来说明,为大家讲清楚这个问题 如下图,是一个4行6列的地图,S为起点,T为终点,P为钥匙,#为障碍物,不可通行
分析:从以上地图分析出,要从起点S开始走,然后拿到钥匙P,最后到达终点T,有两条最短路径可以走,这两条路径分别是:
路径一:总共需要9步
路径二:总共需要7步
算出以上两条路径之后,我们还需要取其中最短的一条路径,即5+2=7。所以蒜头君拿钥匙 回家的最短路径是7。
再次分析:以上地图中有两把钥匙,所以我们需要分别求出经过这两把钥匙再回到家的最短路径,即求出每把钥匙到家到起点的这两个值,然后将其相加,求出某条最短路径。然后将n条回家的路径进行比较,算出蒜头君回家的最短路径。也就是说当算一把钥匙到家和到起点的最短距离时,我们需要应用两次BFS,如果说地图中有非常多把钥匙,那么要进行的BFS次数就非常多了,此时程序会超时。那有没有别的方法可以解决程序超时的问题呢?或者说这道题要怎么优化?
优化的思路:我们可以在BFS的时候将标记数组多开一维度,表示是否已经取得了钥匙的状态。如果到达终点并且取得钥匙的状态被标记,bfs结束,即找到了最短路径。可能你还是无法理解,我们不妨用刚刚那个例子来进行一个讲解,首先,我们有一个4行6列的地图,S为起点,T为终点,.为道路(钥匙和家所在的地方也可以视为道路),#为障碍物,不可通行,如下图所示:
我们来模拟一下BFS的执行过程,大家都知道BFS的执行需要队列和标记数组。为了表示每一个坐标的状态(走到当前坐标时手上是否有钥匙、当前坐标的位置、走到当前坐标时距离起点S经过了多少步),我们需要构建一个结构体struct,结构体的成员包括如下几个:
注意:此处为也可以不为结构体添加构造函数,添加构造函数的目的时方便我们构造结构体变量,不要也行,只不过,构造结构体变量时,就显得复杂一点而已。
模拟步骤:
第一步:起点S在(4,2)处,将其入队,此时队列里有一个元素,并且设置为起点访问过,此时蒜头君身上没有钥匙,此坐标点所表示的队列元素的值为:
第二步:将S点周围的点全部入队,我们不妨约定,按照右 下 左 上的方向进行BFS,要注意看图示内容的文字说明,后面的步骤大同小异
第三步:S点扩展完毕,出队,请注意看图示文字说明
第四步:扩展(4,3)点,如下图所示:
第五步:(4,3)出队,此时队首元素是(3,2),从(3,2)开始扩展
以上第9行第3列写错了,2代表的含义应该是距离S共2步
第六步:扩展(3,2)点,如下图所示:
第七步:(3,2)扩展完毕,出队,如下图所示:
第八步:扩展(3,3)点,如下图所示:
第九步:(3,3)扩展完毕,出队,如下图所示:
第十步:(3,1)是队首元素,开始扩展,如下图所示:
第11步:(3,1)出队,如下图所示
第12步:(2,2)是队首元素,开始扩展,如下图所示:
第13步:(2,2)出队,如下图所示
第14步:(3,4)是队首元素,开始扩展,如下图所示:
第15步:(3,4)出队,如下图所示
第16步:(2,1)是队首元素,开始扩展,如下图所示:
第17步:(2,1)出队,如下图所示
第18步:(1,2)是队首元素,开始扩展,如下图所示:
第19步:(1,2)出队,如下图所示
第20步:(3,5)是队首元素,开始扩展,如下图所示:
第21步:(3,5)出队,如下图所示
第21步:(2,4)为队首元素,开始扩展,发现它不能扩展任何点,出队
第22步:(1,1)为队首元素,开始扩展,注意:从(1,1)这个点开始,蒜头君已经获得了钥匙,所以此时v[x][y][1]这个元素派上用场了,观察图示,你会发现之前用的都是v[x][y][0]
第23步:(1,1)出队,如下图所示
第23步:(1,3)为队首元素,开始扩展,没有点可扩展,(1,3)出队,如下图所示
第24步:(3,6)为队首元素,开始扩展
第25步:(3,6)出队,如下图所示
第26步:(2,5)为队首元素,开始扩展,没有元素可扩展,(2,5)出队,如下图所示
第27步:(1,2)为队首元素,开始扩展
第28步:(1,2)出队,如图所示:
第29步:(2,1)为队首元素,开始扩展
第30步:(2,1)出队,如图所示:
第31步:(3,5)为队首元素,开始扩展,发现,当扩展到该点左边的点的时候,到达终点,可以直接得出最短路径为7
分析总结:我们发现,整个过程只用了1次BFS,便求出了蒜头君拿到钥匙然后回家的最短路径,因为我们引入了一个关键的数据结构—为标记数组多开了一个维度,v[x][y][0]代表没有钥匙时是否访问了v[x][y][0]=0代表(x,y)点在没有钥匙时,也没有被访问,v[x][y][0]=1代表在没有钥匙时,该点被访问,v[x][y][1]=0代表(x,y)点在有钥匙时,没有被访问,v[x][y][1]=1,代表有钥匙,该点被访问。这一段话要仔细去斟酌。
代码实现1:
#include<bits/stdc++.h> using namespace std; /* 算出拿钥匙的路+回家的路的最短距离 思路1:求出钥匙到家到起点的两个值,将二者相加即可,求出最小的即为 最短距离 思路2:较难理解,可以利用excel表格画图 提示:同学们要注意,老师上课讲得一些题目都非常经典,那么都是取自于历年的一些真题,上课以题目为主 通过题目的方式去讲解相关的知识,所以同学们一定要学会相关的一些写法以及相关的一些思路 样例1: 8 10 P.####.#P# ..#..#...# ..#T##.#.# .......... ..##.##### .......... #####...## ###....S## 样例1答案: 21 */ const int N = 2005; int n,m; //地图大小 int sx,sy,ex,ey; //起点和终点 int dir[4][2]={{0,1},{1,0},{0,-1},{-1,0}}; //右 下 左 上 char a[N][N]; //地图是一个二维数组,行列不超过2000 bool v[N][N][2]; //一般搜索都会涉及到一个访问数组,不管是DFS还是BFS,都需要将访问过的点设置为已经访问 // 要特别注意v这个数组的含义,第三维用来标记蒜头君走过某个点的时候是否已经带了钥匙 struct node { int x,y; //坐标 int step; bool flag; //钥匙的状态,1代表有钥匙,0代表没有钥匙 //为了更好进行入队,添加构造函数 node(int xx,int yy,int stepp,bool flagg) { x=xx; y=yy; step=stepp; flag=flagg; } }; queue<node> q; int bfs(int x,int y) { //一开始在起点时步数为0,并且手里没有钥匙,可以用false代表,也可以用0代表 q.push(node(x,y,0,false)); v[x][y][0]=true; //第三维为0,因为时没有钥匙的状态,true代表访问,也可以用1代表 while(!q.empty()) { //获取队首元素 node now = q.front(); q.pop(); //获取队首元素之后,就可以实现出队了,因为队首元素已经保存在now了,不会丢失 //队首可扩展的点进行入队 for(int i=0;i<4;i++) { int tx=now.x+dir[i][0]; int ty=now.y+dir[i][1]; if(tx>=0&&tx<n&&ty>=0&&ty<m&&a[tx][ty]!='#'&&!v[tx][ty][now.flag]) { v[tx][ty][now.flag]=1; if(a[tx][ty]=='P') { q.push(node(tx,ty,now.step+1,true)); } else if(a[tx][ty]=='T'&&now.flag==1) { return now.step+1; } else { q.push(node(tx,ty,now.step+1,now.flag)); //此点是否带了钥匙受队首元素的影响 } } } } } int main() { cin>>n>>m; //n行m列 for(int i=0;i<n;i++) { cin>>a[i]; } //寻找起点S的坐标位置 for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { if(a[i][j]=='S') { sx=i; sy=j; } } } //从起点出发,进行bfs cout<<bfs(sx,sy); return 0; }
代码实现2:
#include<bits/stdc++.h> using namespace std; /* 算出拿钥匙的路+回家的路的最短距离 思路1:求出钥匙到家到起点的两个值,将二者相加即可,求出最小的即为 最短距离 思路2:较难理解,可以利用excel表格画图 提示:同学们要注意,老师上课讲得一些题目都非常经典,那么都是取自于历年的一些真题,上课以题目为主 通过题目的方式去讲解相关的知识,所以同学们一定要学会相关的一些写法以及相关的一些思路 样例1: 8 10 P.####.#P# ..#..#...# ..#T##.#.# .......... ..##.##### .......... #####...## ###....S## 样例1答案: 21 */ const int N = 2005; int n,m; //地图大小 int sx,sy,ex,ey; //起点和终点 int dir[4][2]={{0,1},{1,0},{0,-1},{-1,0}}; //右 下 左 上 char a[N][N]; //地图是一个二维数组,行列不超过2000 bool v[N][N][2]; //一般搜索都会涉及到一个访问数组,不管是DFS还是BFS,都需要将访问过的点设置为已经访问 // 要特别注意v这个数组的含义,第三维用来标记蒜头君走过某个点的时候是否已经带了钥匙 struct node { int x,y; //坐标 int step; bool flag; //钥匙的状态,1代表有钥匙,0代表没有钥匙 //为了更好进行入队,添加构造函数 node(int xx,int yy,int stepp,bool flagg) { x=xx; y=yy; step=stepp; flag=flagg; } }; queue<node> q; void bfs(int x,int y) { //一开始在起点时步数为0,并且手里没有钥匙,可以用false代表,也可以用0代表 q.push(node(x,y,0,false)); v[x][y][0]=true; //第三维为0,因为时没有钥匙的状态,true代表访问,也可以用1代表 while(!q.empty()) { //获取队首元素 node now = q.front(); q.pop(); //获取队首元素之后,就可以实现出队了,因为队首元素已经保存在now了,不会丢失 //判定是否抵达终点 if(a[now.x][now.y]=='T') { if(now.flag) { cout<<now.step<<endl; break; } continue; //到达终点但是没有钥匙,这个点无需再扩展 } //队首可扩展的点进行入队 for(int i=0;i<4;i++) { int tx=now.x+dir[i][0]; int ty=now.y+dir[i][1]; if(tx>=0&&tx<n&&ty>=0&&ty<m&&a[tx][ty]!='#'&&!v[tx][ty][now.flag]) { v[tx][ty][now.flag]=1; if(a[tx][ty]=='P') { q.push(node(tx,ty,now.step+1,true)); } else { q.push(node(tx,ty,now.step+1,now.flag)); //此点是否带了钥匙受队首元素的影响 } } } } } int main() { cin>>n>>m; //n行m列 for(int i=0;i<n;i++) { cin>>a[i]; } //寻找起点S的坐标位置 for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { if(a[i][j]=='S') { sx=i; sy=j; } } } //从起点出发,进行bfs bfs(sx,sy); return 0; }
总结: 如果对以上的讲解内容或者代码有任何疑问的话,可在评论区里回复,我会及时答复,也可以私信我,将你添加到算法群聊中,一起来学习成长。这一期的算法课堂到这里就结束啦!