C++使用BFS求解最短路径
C++使用BFS求解最短路径
- 一、BFS原理简介
- 二、BFS应用——蓝桥杯穿越雷区题目
- 2.1题目概述
- 2.2实现代码
- 2.3蓝桥杯平台提交结果
- 三、BFS应用——蓝桥杯迷宫与陷阱题目
- 3.1题目概述
- 3.2实现代码
- 3.3蓝桥杯平台提交结果
一、BFS原理简介
BFS(Breadth First Search),中文名为广度优先搜索,是一种遍历搜索算法,其特点是按照距离起始点的路径长度进行搜索,即先将第一层的节点搜索完毕,再搜索下一层。
与之对应的是DFS(Depth First Search)深度优先搜索,优先对深度方向进行搜索,先搜索到最底层,然后再返回进行另一条路径的深度搜索。
由于BFS是按层搜索的,到达每一层节点所走的路径长度一定大于到达上一层节点的路径长度,因此可以用于最短路径的求解,但如果节点与节点间存在权重,例如层级间的路径长度不相同,那么BFS就不一定能够找到最短路径。
二、BFS应用——蓝桥杯穿越雷区题目
2.1题目概述
穿越雷区题目为蓝桥杯2015年国赛程序设计题,题目大致要求如下:
- 在一个给定方阵中,要求求出从输入的A点到B点的最短路径;
- 行走过程中有一个限制,需要交替走过“+”、“-”标记的节点。
此题是一个典型的考察BFS算法的题目,只是题目中增加了节点搜索的限制,即搜索节点的下一个节点时必须搜索满足条件的节点,只需对算法进行略微修改就可求解题目。
2.2实现代码
代码使用C++编写,如下:
#include<iostream>
#include<algorithm>
#include<queue>
#include<chrono>
using namespace std;
const int N = 110;
int n, a[N][N],vis[N][N];
int nt[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
pair<int, int>st, ed;
struct node
{
int x, y, cnt;
};
int bfs(int x, int y)
{
queue<node> que;
que.push(node{x,y,0});
vis[x][y]=1;
while(!que.empty())
{
node u=que.front();
if(u.x==ed.first && u.y==ed.second) return u.cnt;
que.pop();
int x=u.x, y=u.y;
for(int i =0; i < 4; i++)
{
int tx=x+nt[i][0],ty=y+nt[i][1];
if(tx < 0 || tx > n-1 || ty < 0 || ty > n-1 || vis[tx][ty] || a[tx][ty]==a[x][y]) continue;
que.push(node{tx,ty,u.cnt+1});
vis[tx][ty]=1;
}
}
return -1;
}
int main()
{
cin>>n;
for(int i =0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
char x;
cin>>x;
if(x=='A') st.first=i,st.second=j,a[i][j]=-1;
if(x=='B') ed.first=i,ed.second=j,a[i][j]=-1;
if(x=='+') a[i][j]=1;
if(x=='-') a[i][j]=0;
}
}
int x=st.first,y=st.second;
cout<<bfs(x,y)<<endl;
return 0;
}
BFS算法通常使用队列结构进行节点存储,由于队列先进先出的特性,会先输出同级的节点,然后添加这一节点的下层相邻节点。本题在添加下层节点时有两个限制:
- 节点坐标不能超出方阵范围;
- 下层相邻节点的标记字符即“+”、“-”不能与当前节点相同;
- 节点只能访问一次,因为如果第二次添加此节点,那么说明走了回路,即便能够走到终点,路径总长一定大于第一次添加此节点时的路径长度。
2.3蓝桥杯平台提交结果
为测试代码正确性,可以在蓝桥杯的刷题平台提交代码,网址为https://www.lanqiao.cn/problems/?first_category_id=1。
穿越雷区题目编号为141,如下图所示:
代码执行结果如下图,可获得满分:
在提交代码测试过程中发现,无法正常输出测试输入数据,例如本题第一个测试用例方阵长宽为10,则应有100个字符,但在代码中尝试输出这100个字符时,只能输出60多个字符,原因暂且未知。
三、BFS应用——蓝桥杯迷宫与陷阱题目
3.1题目概述
题目整体设置的框架与穿越雷区类似,不同之处如下:
- 起点固定为左上角,终点固定为右下角;
- 某些节点为墙壁,无法通过;
- 某些节点为无敌节点,通过此节点获得无敌状态,在接下来的K步内保持这一状态;
- 某些节点为陷阱节点,只有无敌状态才能通过。
本题的关键之处在于有一个无敌状态的设定,该设定使得在某些情况下节点可以重复访问:
- 第二次经过某节点的无敌状态的剩余步数大于第一次经过时的步数,那么此时可以重复访问;
- 因为无敌状态的剩余步数的变大,虽然较第一次增加了一小段路径长度,但有可能穿过陷阱实现总路径长度更小,到达终点。
3.2实现代码
C++代码如下:
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int N=1100;
int a[N][N], vis[N][N], sta[N][N];
int nxt[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
struct node
{
int x,y,cnt,status;
};
int bfs(int x, int y, int cnt, int status, int n, int k)
{
queue<node> que;
que.push(node{x,y,cnt,status});
vis[x][y]=1;
while(!que.empty())
{
node front=que.front();
if(front.x == n - 1 && front.y == n - 1) return front.cnt;
que.pop();
for(int i = 0; i < 4; i++)
{
int nx = front.x + nxt[i][0], ny = front.y + nxt[i][1];
int nsta = max(0, front.status - 1);
if(nx < 0 || nx > n - 1 || ny < 0 || ny > n - 1 || a[nx][ny] == 3) continue;
if(a[nx][ny] == 2 && nsta == 0) continue;
if(a[nx][ny] == 1 && !vis[nx][ny])
{
nsta = k + 1;
que.push(node{nx,ny,front.cnt + 1,nsta});
vis[nx][ny] = 1;
sta[nx][ny] = nsta;
continue;
}
if(vis[nx][ny])
{
if(sta[nx][ny] >= nsta) continue;
que.push(node{nx,ny,front.cnt + 1,nsta});
sta[nx][ny] = nsta;
}
else
{
que.push(node{nx,ny,front.cnt + 1,nsta});
vis[nx][ny] = 1;
sta[nx][ny] = nsta;
}
}
}
return -1;
}
int main()
{
int n, k;
cin>>n>>k;
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
char x;
cin>>x;
if(x == '%') a[i][j]=1;
if(x == 'x') a[i][j]=2;
if(x == '#') a[i][j]=3;
}
}
cout<<bfs(0, 0, 0, 0, n, k)<<endl;
return 0;
}
代码中关键之处如下:
- 无敌节点只能访问一次,因为到达无敌节点后的无敌剩余步数都相同,只会导致路径长度的增加;
- 单独建立一个sta二维数组,存储无敌剩余步数,用于比较判断是否二次添加节点;
- 获得无敌状态后保持K步,那么到达无敌节点时的无敌状态剩余步数应是K+1,这样才能保持K步;
3.3蓝桥杯平台提交结果
迷宫与陷阱题目编号为229,如下图所示:
代码执行结果如下图,可获得满分: