BFS之最短路径

首先要明确这样一个问题,只有边权为 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,期待与你再次相遇

猜你喜欢

转载自blog.csdn.net/m0_64226820/article/details/126340868