• 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

    输入样例:

    1. 5 5
    2. 0 1 0 0 0
    3. 0 1 0 1 0
    4. 0 0 0 0 0
    5. 0 1 1 1 0
    6. 0 0 0 1 0

    输出样例:

    8

    算法分析:
     对样例进行分析, 最短路径如下

     由于每一条边的边权为1, 所以我们可以定义d数组来存储每个点到原点的距离,每次更新一个点时,距离+1 即可,

    同时我们将这个数组初始化为-1, 用于标志该点是否已经被遍历过。

    coding 一下:
     

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. const int N = 110;
    6. typedef pair<int,int> PII;
    7. int n, m;
    8. int g[N][N];//存放地图
    9. int d[N][N];//存放每一个点到起点的距离
    10. PII q[N * N];//手写队列
    11. int bfs()
    12. {
    13. int hh = 0, tt = 0;
    14. q[0] = {0, 0};
    15. memset(d, - 1 ,sizeof d);//初始化距离为- 1;
    16. d[0][0] = 0;
    17. int dx[4] = {-1, 0, 1, 0}, dy[4] = {0,1, 0, -1};
    18. while(hh <= tt)//队列不为空时
    19. {
    20. PII t = q[hh ++];//取队头元素
    21. for(int i = 0;i < 4;i ++)//枚举4个方向
    22. {
    23. int x = t.first + dx[i];
    24. int y = t.second + dy[i];//在边界内且未被遍历过
    25. if( x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
    26. {
    27. d[x][y] = d[t.first][t.second] + 1;
    28. q[ ++ tt ] = { x, y};//新坐标入队
    29. }
    30. }
    31. }
    32. return d[n - 1][m - 1];//输出右下角点距起点的距离
    33. }

    那么我们进一步扩展,若题目要求我们输出最短路径呢?

    我们看到这道题

    迷宫问题2

    给定一个 n×n 的二维数组,如下所示:

    1. int maze[5][5] = {
    2. 0, 1, 0, 0, 0,
    3. 0, 1, 0, 1, 0,
    4. 0, 0, 0, 0, 0,
    5. 0, 1, 1, 1, 0,
    6. 0, 0, 0, 1, 0,
    7. };

    它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。

    数据保证至少存在一条从左上角走到右下角的路径。

    输入格式

    第一行包含整数 n。

    接下来 n 行,每行包含 n 个整数 0 或 1,表示迷宫。

    输出格式

    输出从左上角到右下角的最短路线,如果答案不唯一,输出任意一条路径均可

    按顺序,每行输出一个路径中经过的单元格的坐标,左上角坐标为 (0,0),右下角坐标为 (n−1,n−1)。

    数据范围

    0≤n≤1000

    输入样例:

    1. 5
    2. 0 1 0 0 0
    3. 0 1 0 1 0
    4. 0 0 0 0 0
    5. 0 1 1 1 0
    6. 0 0 0 1 0

    输出样例:

    1. 0 0
    2. 1 0
    3. 2 0
    4. 2 1
    5. 2 2
    6. 2 3
    7. 2 4
    8. 3 4
    9. 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) 我们这样遍历是为了求得路径方案。

    1. int a = n - 1, b = n - 1;
    2. vector s;
    3. while(true)
    4. {
    5. if(a == 0 && b == 0) break;
    6. s.push_back({a, b});
    7. PII t = pre[a][b];
    8. a = t.x, b = t.y;
    9. }
    10. s.push_back({0, 0}); // {0, 0}未被加入
    11. for(int i = s.size() - 1;i >= 0;i --)
    12. printf("%d %d\n",s[i].x, s[i].y);

    因为{0,0}不具有前导点,所以{0,0}点在最后添加

    方案二: 存储在栈中

    思路与数组存储一致,只是换了个容器存储。

    1. stack s;
    2. int x = n - 1, y = n - 1;
    3. while(x || y){
    4. s.push({x, y});
    5. auto t = pre[x][y];
    6. x = t.x, y = t.y;
    7. }
    8. s.push({0, 0});
    9. //打印方案
    10. while(s.size()){
    11. PII t = s.top(); s.pop();
    12. cout << t.x << ' ' << t.y << endl;
    13. }

    其实,还有种更为简便的办法,我们可以让出口作为入口, 入口作为出口

    也就是从{n - 1, n - 1} 进入迷宫,从{0, 0}出迷宫

    方案三:

     所以可以得到以下图示:

    我们可以发现,点{0,0}由pre[0][0] 可以得到点 {1,0}   点{n-2, n-1} 可以得到点{n-1, n-1}, 

    前面的点可以更新出后面的点, 所以我们在打印最短路径时,只需正向枚举打印即可,而不需要开辟额外空间来存储路径。

    所以核心就是: 逆序搜索,正序打印

    代码如下:

    1. PII end = {0, 0};
    2. while (true)
    3. {
    4. printf("%d %d\n", end.x, end.y);
    5. if(end.x == n - 1 && end.y == n - 1) break; //走到终点时停止
    6. end = pre[end.x][end.y];
    7. }

     coding一下:
     

    1. #include
    2. #include
    3. #include
    4. #define x first
    5. #define y second
    6. using namespace std;
    7. typedef pair<int, int> PII;
    8. const int N = 1010, M = N * N;
    9. int n;
    10. int g[N][N]; //记录地图
    11. PII q[M]; //队列
    12. PII pre[N][N]; //判断是否被遍历过, 同时存储上一个点的坐标
    13. void bfs(int sx , int sy)
    14. {
    15. int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    16. int hh = 0, tt = 0;
    17. q[0] = {sx, sy};
    18. memset(pre, - 1, sizeof pre); //初始化为-1 用于表示是否被遍历过
    19. pre[sx][sy] = {0, 0}; //初始点可以随意设置,但不能设置为 -1
    20. while(hh <= tt)
    21. {
    22. PII t = q[hh ++];
    23. for(int i = 0;i < 4;i ++)
    24. {
    25. int a = t.x + dx[i], b = t.y + dy[i];
    26. if(a < 0 || a >= n || b < 0 || b >= n) continue;
    27. if(g[a][b]) continue; //如果是墙,就不能走过去
    28. if(pre[a][b].x != -1) continue; //若该点未被遍历过
    29. q[ ++ tt] = {a, b};
    30. pre[a][b] = t;
    31. }
    32. }
    33. }
    34. int main()
    35. {
    36. scanf("%d", &n);
    37. for(int i = 0;i < n;i ++)
    38. for(int j = 0;j < n;j ++)
    39. scanf("%d", &g[i][j]);
    40. bfs(n - 1, n - 1); // 从终点往起点遍历
    41. PII end = {0, 0};
    42. while (true)
    43. {
    44. printf("%d %d\n", end.x, end.y);
    45. if(end.x == n - 1 && end.y == n - 1) break; //走到终点时停止
    46. end = pre[end.x][end.y];
    47. }
    48. return 0;
    49. }

    方案一和二的代码如下:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #define x first
    6. #define y second
    7. using namespace std;
    8. typedef pair<int,int> PII;
    9. const int N = 1010;
    10. int n;
    11. PII q[N * N], pre[N][N];
    12. int g[N][N];
    13. void bfs(int sx,int sy)
    14. {
    15. int hh = 0, tt = 0;
    16. int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
    17. q[0] = {sx,sy};
    18. memset(pre, -1, sizeof pre);
    19. pre[sx][sy] = {0, 0}; //初始点可以随意设置,但不能设置为 -1
    20. while(hh <= tt)
    21. {
    22. PII t = q[hh ++];
    23. for(int i = 0;i < 4;i ++)
    24. {
    25. int a = t.x + dx[i], b = t.y + dy[i];
    26. if(a < 0 || a >= n || b < 0 || b >= n) continue;
    27. if(pre[a][b].x != -1) continue;
    28. if(g[a][b]) continue;
    29. q[ ++ tt] = {a, b};
    30. pre[a][b] = {t.x,t.y};
    31. }
    32. }
    33. }
    34. int main()
    35. {
    36. cin >> n;
    37. for(int i = 0;i < n;i ++)
    38. for(int j = 0;j < n;j ++)
    39. scanf("%d",&g[i][j]);
    40. bfs(0, 0);
    41. //方案一
    42. int a = n - 1, b = n - 1;
    43. vector s;
    44. while(true)
    45. {
    46. if(a == 0 && b == 0) break;
    47. s.push_back({a, b});
    48. PII t = pre[a][b];
    49. a = t.x, b = t.y;
    50. }
    51. s.push_back({0, 0}); // {0, 0}未被加入
    52. for(int i = s.size() - 1;i >= 0;i --)
    53. printf("%d %d\n",s[i].x, s[i].y);
    54. //方案二
    55. // stack s;
    56. // int x = n - 1, y = n - 1;
    57. // while(x || y){
    58. // s.push({x, y});
    59. // PII t = pre[x][y];
    60. // x = t.x, y = t.y;
    61. // }
    62. // s.push({0, 0});
    63. // //打印方案
    64. // while(s.size()){
    65. // PII t = s.top(); s.pop();
    66. // cout << t.x << ' ' << t.y << endl;
    67. // }
    68. return 0;
    69. }

    希望本文对你有帮助

    该系列会持续更新, 我是Luffy,期待与你再次相遇

  • 相关阅读:
    关于 Invalid bound statement (not found): 错误的解决
    WPF中创建柱状图(数据统计)
    《致新来的你》
    [element-ui] el-dialog和v-viewer的层级问题
    渗透测试漏洞原理之---【失效的访问控制】
    束带机安全使用须知
    C# 设计模式 之 创建型模式、结构型模式、行为型模式概述
    C++初阶作业 Vector作业详解
    基于JavaSwing开发远程控制系统 课程设计 大作业源码 毕业设计
    软件调研、研发、设计、管理、验收文档(全文档整理)
  • 原文地址:https://blog.csdn.net/m0_64226820/article/details/126340868