算法基础系列第三章——层层推进的BFS

简介: 算法基础系列第三章——层层推进的BFS

问题描述

给定一个 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
原题链接:https://www.acwing.com/problem/content/846/

参考代码(C++版本)

#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int,int> PII;
const int N = 110;
int n,m;
int g[N][N],d[N][N]; //存放地图的数组和存放距离的数组
queue<PII> q;
int bfs()
{
    memset(d,-1,sizeof d);//使用<cstring>中的库函数memset快速初始化数组
    d[0][0] = 0;//标记点(0,0)已经走过了
    q.push({0,0});
    //放置的偏移量数组
    int dx[] ={-1,0,1,0},dy[] = {0,1,0,-1};
    //当队列不为空,执行下述操作
    while(q.size())
    {
        auto t = q.front(); //取出队头元素
        q.pop();//已获得队头信息,队头可以出队了
        //对四个方向进行探索
        for(int i = 0; i < 4;i++)
        {
            //利用向量数组获得四个方向的坐标
            int x = t.first+dx[i],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;//更新现在的点(x,y)到起点的距离
                q.push({x,y});//将新的坐标入队
            }
        }
    }
    return d[n-1][m-1];//返回起点到顶点的宽搜结果
}
int main()
{
    cin >> n >>m;
    for(int i = 0; i < n;i++)
        for(int j = 0;j< m;j++) cin >> g[i][j];
    cout << bfs() <<endl;
    return 0;
}

知识储备


宽度优先搜索(Breadth-first search,BFS)


一点闲话


宽度优先搜索也是一种常用的搜索方式之一,和它一起耳熟能详的是深度优先搜索(DFS),都是从某个状态出发,探索所有可以到达的状态


BFS的搜索模式


宽度优先搜索总是先搜索距离初始状态近的状态。也就是说,它是按照这样的顺序进行搜索的,开始状态——>只需转移1次就可以到达的所有状态——>只需转移2次就可以到达的所有状态——>…。对于同一个位置,宽度优先搜索只会经过它一次,以后再碰到它便不会再检索它。


BFS依赖的数据结构


宽度优先搜索不同于深度优先搜索,它是一层一层的进行遍历,因此需要用到的是先入先出的队列


BFS的用武之地


BFS可以用于处理一下问题:


第一类问题:可达性问题。从结点A出发,有前往结点B的路径吗?

第二类问题:最短路问题。从结点A出发,前往结点B的哪条路最短?

由于按照层次逐层进行搜索、遍历,宽度优先搜索时按照距开始状态由近及远的顺序进行搜索,也就常常用来处理最短路、最少操作等问题。

注意:宽度优先搜索只能解决每条边的权值是相同的最短路径问题。其他情况的最短路问题需要使用专门的最短路算法,比如:Dijkstra算法、Bellman-Ford算法等等。这些算法后续我也会持续更出对它们的理解和总结的喔~🎉🎉🎉


解题思路


BFS的运作流程


前提铺垫:使用数组d[]记录个当前点v到起点s的距离


将起点s放入队列Q中(访问)

只要Q不空,循环执行下述操作

从Q中取出顶点u进行访问(访问结束)

将与u相邻的未访问的顶点v放入Q,同时将d[v]更新为d[u] +1


逻辑思路讲解


1.输入:创建地图

cin >> n >>m;
for(int i = 0; i < n;i++)
    for(int j = 0;j< m;j++) cin >> g[i][j];

2.调用bfs()函数

cout << bfs() <<endl;

实现宽搜的bfs()函数

1,前戏——初始化整个距离数组

   memset(d,-1,sizeof d);//使用的是<cstring>库中的memset函数,按照字节对传入的数组全部初始化为-1
    d[0][0] = 0;//表示点(0,0)已经走过了

2, 将起点s放入队列Q中(访问)

q.push({0,0});

3,只要Q不空,循环执行下述操作===> 取队头,拓展队头

int dx[] ={-1,0,1,0},dy[] = {0,1,0,-1};
while(q.size())
   {
       auto t = q.front();
       q.pop();
       for(int i = 0; i < 4;i++)
       {
           int x = t.first+dx[i],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.push({x,y});
           }
       }
   }


难点剖析


最难处理的应该是循环体中的逻辑怎么安排

首先,按照bfs运作流程取出队头.这里使用了c++的关键字auto,它会自动判断返回值类型,偷懒省事必备喔

获得队头信息以后,这个队头就可以出队了,使得下一个信息可以来到队头的位置被获取

        auto t = q.front();
        q.pop()

然后,将与u相邻的未访问的顶点v放入Q 也就是拓展队头,判断队头可以向走到哪些位置。此处取巧使用了向量数组来表示4个移动的方向。将获取后的新坐标放到if判断里,判断其是否越界,是否是可以探索的,是否被探索过。假如都没有,那就占据这个点,也就是将其入队,同时将d[v]更新为d[u] +1

        for(int i = 0; i < 4;i++)
        {
            int x = t.first+dx[i],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.push({x,y});
            }
        }

关于方向向量数组

微信图片_20221017161727.jpg

如图,可以将一个坐标点进行抽象,那么它在x轴上能活动的区域,按照顺时针记录,是{-1,0,1,0}。对于y一样可以这种记录,即{0,1,0,-1}。

使用向量数组可以简化我们的代码,对于四个方向的探索就可以不用麻烦的写if判断


举一反三——信息学奥赛一本通-T1255

给定一个 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

参考代码(C++版本)

#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef pair<int,int> PII;
const int N = 1010;
int n;
int g[N][N];
PII prev_num[N][N];
void bfs(int sx,int sy)
{
    queue<PII> q;
    //入队
    q.push({sx,sy});
    int dx[] = {-1,0,1,0},dy[] = {0,1,0,-1};
    memset(prev_num,-1 ,sizeof prev_num);
    //当队列不为空
    while(q.size())
    {
        //取队头
        PII t = q.front();
        q.pop();
        //拓展队头
        for(int i = 0;i < 4;i++)
        {
            int a = t.first+dx[i],b = t.second+dy[i];
            if(a< 0 || a  >= n || b < 0 || b >= n) continue;//越界
            if(g[a][b]) continue;//墙
            if(prev_num[a][b].first != -1) continue;
            //加到队列
            q.push({a,b});
            //记录上个状态的位置
            prev_num[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.first,end.second);
        if(end.first == n-1 && end.second == n-1) break;
        end = prev_num[end.first][end.second];
    }
    return 0;
}

解题思路


这道迷宫问题和走迷宫几乎是一样的,思路是一样的,用宽度优先搜索一层一层的去查找,第一次搜索到的终点就是最短路径。要求我们输出这段路径。


道理谁都懂,随口就能生动形象的描述出来,从这个点走到哪个点,再转一下…问题怎么用代码实现出来了?


我们来手动模拟一下bfs探索的过程,先取出队头,然后咱们站在队头的位置,眺望四方,康康哪里可以走了?喔~~~ 那里可以走,好的,走上去,记录下当前的位置,再探索。按照这种流程,我们可以开一个数组,记录当前这个点(x,y)是从哪个点走过来的。


详细剖析

微信图片_20221017161924.jpg

记录数据

typedef pair<int,int> PII;
PII prev_num[N][N];
// 记录数据的伪代码如下:
prev_num[x][y] = 探索前的坐标
//记录上个状态的位置
prev_num[a][b] = t;

1。涉及的stl容器:pair(数对)是将2个数据组合成一组数据。pair的实现是一个结构体,主要的两个成员变量first和second,分别存储两个数据。因为我们要输出的是一组坐标,那么用数对pair来记录会让我们,如鱼得水

2.关于prev_num[x][y] = 探索前的点,有小伙伴读起来可能比较蒙,what’s this ?!各位看官,可这种理解喔

现在有一个二维矩阵,矩阵的类型是存放一组int型变量的数对,矩阵的名字是prev_num。现在让矩阵中位置是(x,y)的这块空间存放探索前的坐标t(这个坐标肯定也是数对类型的啦~)


输出数据


经过一番倒腾,数据终于记录进去了,现在怎么输出这个二维数组了?注意喔,正是因为它是二维数组,所以不能很暴力的直接两个for循环输出结果。因为其他没有用到的位置也会被遍历输出。


这里就出现了比较巧妙的一点,bfs搜索的时候,倒着搜索回去。那我存放的点就是从起点(0,0)逐步逐步的到达终点,这样我们就可以正向遍历这段路径了

    bfs(n-1,n-1);//倒着搜回去
    PII end(0,0);//创建一个PII类型的变量end,初始化为(0,0)
    while(true)
    {
        printf("%d %d\n",end.first,end.second);
        if(end.first == n-1 && end.second == n-1) break;
        end = prev_num[end.first][end.second];
    }


相关文章
|
7月前
|
存储 算法 容器
【优选算法专栏】专题十六:BFS解决最短路问题(二)
【优选算法专栏】专题十六:BFS解决最短路问题(二)
72 1
|
7月前
|
算法
【优选算法专栏】专题十六:BFS解决最短路问题(一)
【优选算法专栏】专题十六:BFS解决最短路问题(一)
68 0
|
7月前
|
消息中间件 算法 NoSQL
BFS - 常见算法问题
BFS - 常见算法问题
|
7月前
|
人工智能 算法 BI
【优选算法专栏】专题十八:BFS解决拓扑排序(一)
【优选算法专栏】专题十八:BFS解决拓扑排序(一)
53 0
|
7月前
|
存储 算法
【优选算法专栏】专题十六:BFS解决最短路问题---前言
【优选算法专栏】专题十六:BFS解决最短路问题---前言
63 1
|
7月前
|
算法
【优选算法专栏】专题十八:BFS解决拓扑排序--前言
【优选算法专栏】专题十八:BFS解决拓扑排序--前言
56 1
|
6月前
|
存储 机器学习/深度学习 算法
第十五届蓝桥杯pb组国赛E题[马与象] (15分)BFS算法 详解
第十五届蓝桥杯pb组国赛E题[马与象] (15分)BFS算法 详解
66 3
|
6月前
|
存储 算法 Java
Java中,树与图的算法涉及二叉树的前序、中序、后序遍历以及DFS和BFS搜索。
【6月更文挑战第21天】Java中,树与图的算法涉及二叉树的前序、中序、后序遍历以及DFS和BFS搜索。二叉树遍历通过访问根、左、右子节点实现。DFS采用递归遍历图的节点,而BFS利用队列按层次访问。以下是简化的代码片段:[Java代码略]
50 4
|
2月前
|
机器学习/深度学习 存储 算法
数据结构与算法——BFS(广度优先搜索)
数据结构与算法——BFS(广度优先搜索)
|
4月前
|
存储 算法
BFS算法的实现
BFS算法的实现
55 1
下一篇
DataWorks