广度优先搜索-算法入门

广度优先搜索(BFS)-算法入门

广度优先搜索概述

广度优先搜索本质上,其实是一种迭代的思想,通过源点扩散地去搜索其他的点,搜到的点用队列来维护,再用这些点继续扩散,从而达到遍历全局;形象来说的话,可以想象成新冠疫情的扩散一样,先从一个城市爆发,如果不加以管制,它便扩散到它临近的城市,再以临近的城市为据点,继续扩散。这里就要再提一下深度优先搜索(DFS)了,深度优先搜索的本质是递归来实现的,它的实现上,不像BFS的扩散;形象地解释的话,感觉它有点像我们的打水井一样,一直打到底,打通了,从井里回来,继续打下一个;打不通,也从井里回来,继续打下一个,直到全部的打井点打完。广度优先搜索(BFS),我们需要关注的细节主要有四点,扩散类型扩散方式扩散条件扩散行为下面用一些例题来了解,BFS具体是如何去实现的。

走迷宫

题目描述:
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。

最初,有一个人位于左上角 (1,1)(1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角 (n,m)(n,m) 处,至少需要移动多少次。

数据保证 (1,1)(1,1) 处和 (n,m)(n,m) 处的数字为 0,且一定至少存在一条通路。

示例:

输入: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

解决方案:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N=110;
typedef pair<int,int> pll;
int d[N][N],g[N][N];
int n,m;
int bfs(){
    queue<pll> q;
    q.push({0,0});
    memset(d,-1,sizeof(d));//给d全局赋值
    d[0][0]=0;
    int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
    while(q.size()){
        pll t=q.front();
        q.pop();//两条语句相当于t=q[hh++]
        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});//相当于队列中q[++tt]=({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;
}

分析:首先来观察它的扩散类型,它的源点即是一个二维的坐标,所以我们用pair<int, int>来定义;再来看它的扩散方式,上下左右四个方向扩散,我们用dx[4],dy[4]和一个for循环来表示它的移动;最后它的扩散条件①一个是不能超过地图边界②访问过的点不再访问③是遇到墙壁不能走;一般情况下,前两点都是包含在扩散条件之中的;扩散行为(扩散后要干什么)我们的目的是获得扩散到终点走的步数,那么我们每扩散一步,扩散得到的点的步数 = 扩散前那个点的步数 + 1;结束条件,队列为空(即已经全部扩散到了,不能再往下扩散)。

接下来看一道多源点扩散的题目

岛屿数量

题目描述:
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

解决方案:

class Solution {
public:
    const int dx[4] = {0,-1,1,0};
    const int dy[4] = {1,0,0,-1};
    int numIslands(vector<vector<char>>& grid) {
        int count = 0;
        int m = grid.size(), n = grid[0].size();//求行和列的值
        vector<vector<int>> grid_(m,vector<int>(n));
        for (int i = 0; i < m; i++)
         for (int j = 0; j < n; j++)
           grid_[i][j]=grid[i][j] - '0';
        bool visited[m][n];
        memset(visited,false,m * n);
        queue<pair<int,int>> q;
        for (int i = 0; i < m; i++)
         for (int j = 0; j < n; j++) {
            if (!visited[i][j]&&grid_[i][j]==1){
                 q.emplace(i,j);
                 visited[i][j] = true;
                while (!q.empty()) {
                     int x = q.front().first, y = q.front().second;
                     q.pop();
                    for (int k = 0; k < 4; k++) {
                         int mx = x + dx[k], my = y + dy[k];
                       if (mx>=0&&mx<m&&my>=0&&my<n&&!visited[mx][my]&&grid_[mx][my]==1) {
                           q.emplace(mx,my);
                           visited[mx][my] = true;
                         }
                     }
                 }
                 count++;
             }
         }
         return count;
    }
};
//q.emplace()和q.push()差不多,就是前者的功能更全面一些,但版本老一点的编译器似乎不支持,具体区别可以自己去网上查查看啦^ _ ^

分析:

扩散类型:若干个二维的陆地坐标(grid[][] = = 1)

扩散方式:上下左右四个方向

扩散条件:①一个是不能超过地图边界 ②访问过的点不再访问 (visited[x][y] == false)③扩散得到的区域是陆地

扩散行为:用一个计数器count初始为0,每扩散得到一片相通的岛屿即一块陆地,便让count++

再扩散到一块陆地后,我们再去寻找新的没有被扩散到的岛屿(用两层循环实现),以它为源点,继续扩散,直到结束。

再看一道多源点已知的题目

01矩阵

题目描述:
给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。两个相邻元素间的距离为 1 。

输入:mat = [[0,0,0],[0,1,0],[0,0,0]]
输出:[[0,0,0],[0,1,0],[0,0,0]]

输入:mat = [[0,0,0],[0,1,0],[1,1,1]]
输出:[[0,0,0],[0,1,0],[1,2,1]]    

解决方案:

class Solution {
public:
    const int dx[4] = {0,0,1,-1};
    const int dy[4] = {1,-1,0,0};
    vector<vector<int>> updateMatrix(vector<vector<int>>& mat) {
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> dist(m, vector<int>(n));
        queue<pair<int, int>> q;
        bool visited[m][n];
        memset(visited, false, m * n);
        for (int i = 0; i < m; i++)
         for (int j = 0; j < n; j++) 
             if (!mat[i][j]){
                 q.emplace(i,j);
                 visited[i][j] = true;
             }//将所有的0弹入队列中
                 while (!q.empty()) {
                    int x =q.front().first, y =q.front().second;
                    q.pop();
                    for (int k = 0; k < 4; k++) {
                        int mx = x + dx[k], my = y + dy[k];
                        if (mx>=0&&mx<m&&my>=0&&my<n&&!visited[mx][my]){
                            dist[mx][my] = dist[x][y] + 1;
                            q.emplace(mx,my);
                            visited[mx][my] = true;
                        }
                    }
                }
         return dist;
    }
};

分析:
仔细观察,这个题目其实是前两道题目的结合版,四大细节基本和第一个题目相同,但它是一个和第二题一样的多源点题目,以多个0的位置为起点,找到最近的1的距离,我们一开始便将全部的0的位置加入队列中,去找到非0的位置,具体细节和第一题相仿,然后再输出dist即可,学过最短路径的朋友,是不是会觉得很像多源最短路径hh。看到这里,如果都弄懂了的话,可以用下面这题练练手,比上面的题目应该要稍微难一点,自己做出来了就会很有成就感啦。

腐烂的橘子

之前看到的题目的扩散类型都是坐标,也是比较常见的BFS解决类型,接下来看看与树的结点相关的扩散

填充每一个结点的下一个右侧结点

题目描述:

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。初始状态下,所有 next 指针都被设置为 NULL。

在这里插入图片描述

输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束

解决方案:

class Solution {
public:
    Node* connect(Node* root) {
        if (!root) return root;
        queue<Node*> q;
        q.emplace(root);
        while (!q.empty()) {
            int n = q.size();
            for (int i = 0; i < n; i++) {//遍历每一层的点
                Node* t = q.front();
                q.pop();
                if (i < n -1) t->next = q.front();//n-1时不修改
                if (t->left) q.emplace(t->left);
                if (t->right) q.emplace(t->right);
            }
        }
        return root;
    }
};

分析:

扩散类型:树的结点

扩散方式:一层一层地扩散(可以理解为层次遍历)

扩散条件:只要当前结点有左节点或右节点就继续扩散

扩散行为:只要不是当前这一层的最后一个结点,就将当前扩散到结点t的next指针修改为它在树中的右边的结点(即在队列中q.pop()后的队首结点)

这个题目的空间复杂度其实可以降到常数级别,直接用层次遍历的方法,感兴趣可以看看,代码放在下面:

class Solution {
public:
    Node* connect(Node* root) {
        if (!root) return root;
        Node* leftm = root;//leftm为每一层最左边结点
        while (leftm->left) {
            Node* t = leftm;//因为leftm要被修改,我们需要用t暂存leftm的值,方便遍历下一层        
            while (t) {
                t->left->next = t->right;
                if (t->next) t->right->next = t->next->left;//t的右节点的next指针修改为,t的next指针的左节点(可以看图用笔比划一下)
                t = t->next;
            }
            leftm = leftm->left;//到下一层去
        }
            return root;
    }
};

其他:BFS实现与DFS实现对比 合并二叉树

题目描述:
给你两棵二叉树: root1 和 root2 。想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。返回合并后的二叉树。注意: 合并过程必须从两个树的根节点开始。在这里插入图片描述

输入:root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出:[3,4,5,5,4,null,7]

输入:root1 = [1], root2 = [1,2]
输出:[2,2]

解决方案:

BFS(迭代)

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* r1, TreeNode* r2) {
            if (!r1 || !r2) {//有空树,直接返回非空的树
            return r1 == NULL? r2 : r1;
        }
        queue<TreeNode* > qt;
        qt.emplace(r1), qt.emplace(r2);
        while(!qt.empty()) {
            TreeNode* t1 = qt.front();
            qt.pop();
            TreeNode* t2 = qt.front();
            qt.pop();
            t1->val += t2->val;
            if (t1->left && t2->left) {
                qt.emplace(t1->left);
                qt.emplace(t2->left);
            }
            else if (!t1->left){
                t1->left = t2->left;
            }
            if (t1->right && t2->right) {
                qt.emplace(t1->right);
                qt.emplace(t2->right);
            }
            else if (!t1->right) {
                t1->right = t2->right;
            }
        }
        return r1;
    }
};

DFS(递归)

TreeNode* dfs(TreeNode* r1, TreeNode* r2) {
        if (!r1 || !r2) {
            return r1 == NULL ? r2 : r1;
        }
        r1->val += r2->val;
        r1->left = dfs(r1->left, r2->left);
        r1->right = dfs(r1->right, r2->right);
        return r1;
    };
class Solution {
public:
    TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
        if (!root1 || !root2) {
            return root1 == NULL? root2 : root1;
        } 
        return dfs(root1, root2);  
    }
};

分析:题目的具体情况比较简单,如果两棵树左右子节点处都存在结点,将它们的值相加,如果第一棵树上的左结点没有,把第二棵上的结点搬过来,右结点同理。

BFS运行情况

在这里插入图片描述

DFS运行情况在这里插入图片描述

结论:
可以看到BFS的运行速度显然更快(28ms比36ms),但DFS运行更慢,空间复杂度上差不多(31.7MB比31.4MB),,因为递归的底层需要系统堆栈,效率较低。但DFS也有一个很明显的优势代码简洁。如果大家还想进一步理解BFS(迭代),DFS(递归)的区别建议大家可以去看看这篇文章 递归与迭代的对比,更加深层地去了解他们。

总结

BFS是一种以迭代为基本方法的搜索方式,主要实现细节有扩散类型扩散方式扩散条件扩散行为(个人认为^ o ^)。其实现代码较为复杂,但思路较为清晰,相较于递归的递推和回溯的方式,BFS更容易理解,而且运行速度较快,我个人更喜欢用BFS的方式做题,但是在一些特定的复杂情况下,用BFS的方式模拟过于复杂,我们也需要了解DFS的方式,即使它的内部比较难以理解。
终于写完了,累瘫在这里插入图片描述

参考文献

以上题目来自leetcodeAcWing

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值