首先要明确这样一个问题,只有边权为 1 时才能用BFS求最短路。
若边权不为1,移步图论算法解决。
基于之前所学的Flood Fill 算法,(12条消息) BFS 之Flood Fill 算法_Dream.Luffy的博客-CSDN博客
我们可以扩展出BFS对最短路径问题的求解。
由BFS Flood Fill算法的更新方式,我们可以得知 每一次更新到的点 的距离一定是最小的。
下面我们直接用几个例子来说明。
例一: 走迷宫
给定一个 n×m的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1) 处和 (n,m)处的数字为 0,且一定至少存在一条通路。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围
1≤n,m≤100
输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
8
算法分析:
对样例进行分析, 最短路径如下
由于每一条边的边权为1, 所以我们可以定义d数组来存储每个点到原点的距离,每次更新一个点时,距离+1 即可,
同时我们将这个数组初始化为-1, 用于标志该点是否已经被遍历过。
coding 一下:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 110;
typedef pair<int,int> PII;
int n, m;
int g[N][N];//存放地图
int d[N][N];//存放每一个点到起点的距离
PII q[N * N];//手写队列
int bfs()
{
int hh = 0, tt = 0;
q[0] = {0, 0};
memset(d, - 1 ,sizeof d);//初始化距离为- 1;
d[0][0] = 0;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0,1, 0, -1};
while(hh <= tt)//队列不为空时
{
PII t = q[hh ++];//取队头元素
for(int i = 0;i < 4;i ++)//枚举4个方向
{
int x = t.first + dx[i];
int y = t.second + dy[i];//在边界内且未被遍历过
if( x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
{
d[x][y] = d[t.first][t.second] + 1;
q[ ++ tt ] = { x, y};//新坐标入队
}
}
}
return d[n - 1][m - 1];//输出右下角点距起点的距离
}
那么我们进一步扩展,若题目要求我们输出最短路径呢?
我们看到这道题
迷宫问题2
给定一个 n×n 的二维数组,如下所示:
int maze[5][5] = {
0, 1, 0, 0, 0,
0, 1, 0, 1, 0,
0, 0, 0, 0, 0,
0, 1, 1, 1, 0,
0, 0, 0, 1, 0,
};
它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。
数据保证至少存在一条从左上角走到右下角的路径。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含 n 个整数 0 或 1,表示迷宫。
输出格式
输出从左上角到右下角的最短路线,如果答案不唯一,输出任意一条路径均可。
按顺序,每行输出一个路径中经过的单元格的坐标,左上角坐标为 (0,0),右下角坐标为 (n−1,n−1)。
数据范围
0≤n≤1000
输入样例:
5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
0 0
1 0
2 0
2 1
2 2
2 3
2 4
3 4
4 4
算法分析:
要打印路径,那么就需要存储路径,用数组存吗?可是我们并不知道最短路是哪一条。
所以这里,我们可以用一个数组pre 来存储 点{x,y} 是由哪个点扩展得到的, 也就是前驱节点。
例如 {1, 0} 是 由{0, 0} 扩展所得到的点, 所以pre[1][0] = {0, 0}
但是,这些点存在pre数组里,我们怎么将它打印成路径呢?
我们发现,由{0,0} 并不能得到{1, 0}以及后面的路径, 而{1, 0}却能得到{0,0},所以我们想到从后往前遍历, 存储每一个点,此时我们存储到的是倒序路径,所以最后再倒序枚举一遍即可
我们可以先将这些点依次遍历存在数组中,或栈中
方案一:存储在数组中
因为我们 是由后面的点存储前面的点坐标的 ,所以为了知道前面点的坐标,我们采取从后往前遍历。
每次将pre中的点加入vector中,最后再倒序枚举一遍即得到路径。
此时我们的遍历路径是绿色的这条线。(注意不要把pre的遍历路径和 迷宫路径混淆了, 迷宫路径是从0, 0 -> n - 1, n - 1) 我们这样遍历是为了求得路径方案。
int a = n - 1, b = n - 1;
vector<PII> s;
while(true)
{
if(a == 0 && b == 0) break;
s.push_back({a, b});
PII t = pre[a][b];
a = t.x, b = t.y;
}
s.push_back({0, 0}); // {0, 0}未被加入
for(int i = s.size() - 1;i >= 0;i --)
printf("%d %d\n",s[i].x, s[i].y);
因为{0,0}不具有前导点,所以{0,0}点在最后添加
方案二: 存储在栈中
思路与数组存储一致,只是换了个容器存储。
stack<PII> s;
int x = n - 1, y = n - 1;
while(x || y){
s.push({x, y});
auto t = pre[x][y];
x = t.x, y = t.y;
}
s.push({0, 0});
//打印方案
while(s.size()){
PII t = s.top(); s.pop();
cout << t.x << ' ' << t.y << endl;
}
其实,还有种更为简便的办法,我们可以让出口作为入口, 入口作为出口
也就是从{n - 1, n - 1} 进入迷宫,从{0, 0}出迷宫
方案三:
所以可以得到以下图示:
我们可以发现,点{0,0}由pre[0][0] 可以得到点 {1,0} 点{n-2, n-1} 可以得到点{n-1, n-1},
前面的点可以更新出后面的点, 所以我们在打印最短路径时,只需正向枚举打印即可,而不需要开辟额外空间来存储路径。
所以核心就是: 逆序搜索,正序打印
代码如下:
PII end = {0, 0};
while (true)
{
printf("%d %d\n", end.x, end.y);
if(end.x == n - 1 && end.y == n - 1) break; //走到终点时停止
end = pre[end.x][end.y];
}
coding一下:
#include<iostream>
#include<cstring>
#include<iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n;
int g[N][N]; //记录地图
PII q[M]; //队列
PII pre[N][N]; //判断是否被遍历过, 同时存储上一个点的坐标
void bfs(int sx , int sy)
{
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int hh = 0, tt = 0;
q[0] = {sx, sy};
memset(pre, - 1, sizeof pre); //初始化为-1 用于表示是否被遍历过
pre[sx][sy] = {0, 0}; //初始点可以随意设置,但不能设置为 -1
while(hh <= tt)
{
PII t = q[hh ++];
for(int i = 0;i < 4;i ++)
{
int a = t.x + dx[i], b = t.y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= n) continue;
if(g[a][b]) continue; //如果是墙,就不能走过去
if(pre[a][b].x != -1) continue; //若该点未被遍历过
q[ ++ tt] = {a, b};
pre[a][b] = t;
}
}
}
int main()
{
scanf("%d", &n);
for(int i = 0;i < n;i ++)
for(int j = 0;j < n;j ++)
scanf("%d", &g[i][j]);
bfs(n - 1, n - 1); // 从终点往起点遍历
PII end = {0, 0};
while (true)
{
printf("%d %d\n", end.x, end.y);
if(end.x == n - 1 && end.y == n - 1) break; //走到终点时停止
end = pre[end.x][end.y];
}
return 0;
}
方案一和二的代码如下:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;
const int N = 1010;
int n;
PII q[N * N], pre[N][N];
int g[N][N];
void bfs(int sx,int sy)
{
int hh = 0, tt = 0;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
q[0] = {sx,sy};
memset(pre, -1, sizeof pre);
pre[sx][sy] = {0, 0}; //初始点可以随意设置,但不能设置为 -1
while(hh <= tt)
{
PII t = q[hh ++];
for(int i = 0;i < 4;i ++)
{
int a = t.x + dx[i], b = t.y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= n) continue;
if(pre[a][b].x != -1) continue;
if(g[a][b]) continue;
q[ ++ tt] = {a, b};
pre[a][b] = {t.x,t.y};
}
}
}
int main()
{
cin >> n;
for(int i = 0;i < n;i ++)
for(int j = 0;j < n;j ++)
scanf("%d",&g[i][j]);
bfs(0, 0);
//方案一
int a = n - 1, b = n - 1;
vector<PII> s;
while(true)
{
if(a == 0 && b == 0) break;
s.push_back({a, b});
PII t = pre[a][b];
a = t.x, b = t.y;
}
s.push_back({0, 0}); // {0, 0}未被加入
for(int i = s.size() - 1;i >= 0;i --)
printf("%d %d\n",s[i].x, s[i].y);
//方案二
// stack<PII> s;
// int x = n - 1, y = n - 1;
// while(x || y){
// s.push({x, y});
// PII t = pre[x][y];
// x = t.x, y = t.y;
// }
// s.push({0, 0});
// //打印方案
// while(s.size()){
// PII t = s.top(); s.pop();
// cout << t.x << ' ' << t.y << endl;
// }
return 0;
}
希望本文对你有帮助
该系列会持续更新, 我是Luffy,期待与你再次相遇