【BFS算法】广度搜索·由起点开始逐层向周围扩散求得最短路径(算法框架+题目)

news2024/9/18 13:13:01

0、前言

深度优先搜索是DFS(Depth Frst Search),其实就是前面所讲过的回溯算法,它的特点和它的名字一样,首先在一条路径上不断往下(深度)遍历,获得答案之后再返回,再继续往下遍历。这也是递归的思想,所以这也是为什么回溯算法通常都是用递归来写,而下面的BFS由于不是这种思路从而没有用递归。

广度优先算法(Breath First Search)其实和深度优先算法是一对兄弟,因为它们的解空间都是树形解空间,并且都是在求解过程中 动态生成树形解空间。广度优先算法在于,它在动态生成解空间从而获得答案时,是从一层一层(广度的)取构建并搜索答案。
在这里插入图片描述
它的核心思想是:把问题的求解抽象为一个图,从一个起点开始,向四周去扩散,不断扩散直至求得答案。

BFS和DFS的主要区别在于:

  • 在查找路径问题上,BFS查找出来的路径是最短的
  • BFS的空间复杂度要比DFS高(这也是BFS的代价)

一、算法框架

知道它的核心思想之后,其实我们就可以写出它的算法框架,本质就是遍历图

//计算起点start到终点target的最短距离
int BFS(Node start, Node target) {
    queue<Node> q; //核心数据结构
    set<Node> visited; //避免走回头路
    
    q.push(start);  //将起点加入队列
    visited.insert(start);
	
	//走完一次完整的下面1,2,3循环,代表以当前层节点完整的依次向外扩散了一层
	//循环1:从起点开始,不断将当前队列中所有节点向四周扩散
    while (!q.empty()) {
        int sz = q.size();
        //2:循环2:队列中所有节点依次往外扩散一层
        for (int i = 0; i < sz; i++) {
            Node cur = q.front();
            q.pop();
            if (cur == target)
                return step;
             //循环3:把当前层的队列节点往外扩散一层,并把扩散得到的加入队列
            for (Node x : cur.adj()) {
                if (visited.count(x) == 0) {
                    q.push(x);
                    visited.insert(x);
                }
            }
        }
    }
    // 如果走到这里,说明在图中没有找到目标节点
}

  • 其中Queue<Node> q队列,存储当前遍历层和即将遍历层的节点,是BFS的核心数据结构;
  • cur.adj()泛指与当前遍历到的这个节点cur相邻的节点(也就是与cur相连的下一层的节点,不如二维数组中cur上下左右四个位置就是相邻节点);
  • visited的作用防止走回头路;(但是对于二叉树这种结构,没有子节点到父节点的指针,不会走回头路也就不需要visited)
    在这里插入图片描述

二、题目

2.1:二叉树的最小高度

1.题目

🔗111. 二叉树的最小深度

在这里插入图片描述

2.思路

这道题就是非常典型和简单的BFS算法,我们只需要把上面算法框架中的cur == target找到目标答案的位置改成找到叶子节点:cur.left == null && cur.right == null即可

3.代码

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int minDepth(TreeNode* root) {
        if(root == nullptr) return 0;
        
        queue<TreeNode*> q; //核心数据结构,用来存储当前节点
        int depth = 0; //记录当前层的深度
        //将第一层输入队列:即起点
        q.push(root);

        //从当前层逐层扩散和遍历:循环1:遍历层
        while(!q.empty()){
            //获取当前层所以节点数量
            int sz = q.size();
            depth++;
            //循环2:遍历当前层的所有节点
            for(int i = 0; i < sz; i++){
                TreeNode* cur = q.front();
                q.pop();

                //判断当前遍历到的这个节点释放符合答案需求
                if(cur->left == nullptr && cur->right == nullptr)
                    return depth;
                
                //循环3:以当前层的当前节点往外再扩一层,并把扩散得到的下一层节点加入队列
                //这里就已经确定了是左右孩子两个相邻节点,就不写循环了
                if(cur->left!=nullptr) q.push(cur->left);
                if(cur->right!=nullptr) q.push(cur->right);
            }
        }
        return 0;
    }
};

2.2:解开密码锁的最少次数

1.题目

🔗752. 打开转盘锁

在这里插入图片描述

2.思路

  • 分析:题目要求我们从0000开始,根据旋转规则,每次只能向上或向下旋转一次四个数位的其中一位,通过不断的旋转直到四位数字为最终答案target
  • 关键:把问题抽象成一张图,当前4位数字密码为节点,通过当前密码状态选择一个数位向上或向下旋转一次得到下一个四位数(节点),每个节点可以实现2x2x2x2 = 8种选择得到下一个节点;即:该问题抽象为一张图,每个节点有8个相邻节点,我们需要从起始节点"0000"以最短路径寻找到我们的目标节点target (注意还有限制条件)

在这里插入图片描述
有了上述分析后,大概有对这个题有了一个这个题的把握和认识,但是其实还有很多细节没有分析到。当然,一下子其实分析不到所有的细节是很正常,我们一步一步来,慢慢完善。

  1. 首先,我们先不考虑【死亡密码】的限制,想一想,从"0000"出发,穷举出所有密码的可能,我们需要怎么做;也就是说,从"0000"出发,每个节点有8个相邻,我们以BFS遍历出所有密码
//BFS框架,打印出所有可能的密码
void BFS(string target){
	queue<string> q;
	q.push("0000");
	
	while(!q.empty()){
		int sz = q.size();
		
		//将当前层(也就是当前队列)中节点向下一层(周围)扩散
		for(int i = 0; i < sz; i++){
			//获取当前节点
			string cur = q.front(); q.pop();
			//判断是否达到终点
			cout << cur << endl;
			
			//将当前节点相邻的周围节点(也就是下一层的)加入到队列
			for(int j = 0; j < 4; j++){
				//当前位向上拨
				string up = plusOne(cur, j);
				q.push(up);
				//当前位向下拨
				string down = minusOne(cur, j);
				q.push(down);
			}
		}
		//在这里增加步数
	}
}

上面代码中的plusOneminusOne函数的代码如下:

string plusOne(string s, int j) {
    if (s[j] == '9') s[j] = '0';
    else s[j] += 1;
    return s;
}

string minusOne(string s, int j) {
    if (s[j] == '0') s[j] = '9';
    else s[j] -= 1;
    return s;
}
  1. 当然,上述代码还有很多问题
  • 首先,会走回头路,比如从"0000""1000"但是"1000"的8个相邻点里面又有"0000"这样下去会产生死循环。这个时候就需要额外加一个集合数据结构visited,记录下来已经走过的节点,在往下扩散的时候(也就是往队列里面加元素的时候)进行判断,是否已经遍历过。
  • 没有终止条件,这个其实挺好改的,只需要在遍历到那个具体的节点的时候加一个判断条件,遇到target结束循环并返回即可。
  • 没有对限制条件(不能出现死亡密码)的处理

3.代码

class Solution {
public:
  string plusOne(string s, int j) {
    if (s[j] == '9') s[j] = '0';
    else s[j] += 1;
    return s;
}

string minusOne(string s, int j) {
    if (s[j] == '0') s[j] = '9';
    else s[j] -= 1;
    return s;
}
    int openLock(vector<string>& deadends, string target) {
        unordered_set<string> deads; //记录限制条件:需要跳过的死亡密码
        for(string s : deadends){
            deads.insert(s);
        }
        //记录已经穷举过的密码,防止走回头路
        unordered_set<string> visited;
        queue<string> q;
        int step = 0;
        //从起点开始进行BFS
        q.push("0000");
        visited.insert("0000");
        while(!q.empty()){
            int sz = q.size();

            for(int i = 0; i < sz; i++){
                string cur = q.front();
                q.pop();

                if(deads.count(cur)) continue; //分支限界,在遍历到这个节点的时候限制就行
                //判断是否达到终止条件
                if(cur == target) return step;

                for(int j = 0; j < 4; j++){
                    string left = plusOne(cur,j);
                    if(!visited.count(left) ){
                         q.push(left);
                         visited.insert(left);
                    }
                    string right = minusOne(cur,j);
                    if(!visited.count(right)) {
                        q.push(right);
                        visited.insert(right);
                    }
                }
            }
            step++;
        }
        return -1;

    }
};

2.3:布线问题

1.题目

印刷电路板将布线区域划分成n*m个方格阵列,精确的电路布线问题要求确定连接方格a的中点到方格b的中点的最短布线问题。在布线时,电路只能沿着直线或直角布线。为了避免线路相交,已布了线的方格做了封锁标记,其他线路不允许穿过被封锁的方格。
在这里插入图片描述

2.思路

  • 分析:其实这个题就是有障碍物的迷宫问题。本质上也是抽象为图问题,格子上每一个点的坐标位置就是一个节点,一个节点可以往上下左右4个方向行走到达其他位置(节点);注意限制条件:不能在障碍物(也就是题目所说的封锁方格)处进行遍历,所以遇到该限制条件就跳过且不扩展该样的节点。
  • 关键:
  1. 用一个队列来进行BFS搜索,找到起点位置到终点位置的最短路径
  2. 用一个二维布尔数组visited来存储对应位置处方格是否被访问过(避免走回头路)
  3. 用一个二维数组来记录封锁方格(其实就是存储初始棋盘grid[][]
  4. 如果需要记录最终得到的那个最短路径,可以设置一个以pair<int, int>为元素的二维数组,记录(x,y)位置是由parent[x][y]得来的(记录路径,能从目标节点反推回去完整路径)

3.代码

#include <iostream>
#include <vector>
#include <queue>
#include <tuple>

using namespace std;

//定义节点可以扩展的四个方向:上下左右{x,y}
vector< pair<int, int> > dirs ={{-1,0}, {1,0}, {0,-1}, {0, 1}};

/*辅助函数,判断该节点是否合法
    1. 不能超过整个迷宫(电线板)边界
    2. 不能被访问过(不能走回头路)
    3. 不能是封锁节点(即迷宫里的障碍物
*/
bool isValid(int x, int y, int n, int m, vector<vector<int>>& grid, vector <vector<bool>>& visited){
    return x >= 0 && x <n && y>=0 && y < m && grid[x][y] != -1 && !visited[x][y];
}

//BFS算法求解最短路径并标记路径节点
vector <vector<int>> shortestPath(vector<vector<int>>& grid, pair<int, int> Start, pair<int, int> End){
    int n = grid.size();
    int m = grid[0].size();

    //队列,存储当前坐标和部署
    queue<tuple<int, int, int>> q;
    //已经访问节点的存储
    vector<vector<bool>> visited(n, vector<bool>(m, false));
    //记录当前节点是由哪个节点得来(记录路径)
    vector<vector<pair<int, int>>> parent(n, vector<pair<int, int>>(m, make_pair(-1, -1)));

    //将起点加入队列
    q.push(make_tuple(Start.first, Start.second, 0));
    visited[Start.first][Start.second] = true;

    bool found = false; //标记是否找到,一旦找到,路径最短, 退出

    while(!q.empty()){

        int sz = q.size();
        //遍历该层
        for(int i = 0; i < sz; i++){
            //先拿出当前节点
            auto [cur_x, cur_y, steps] = q.front();
            q.pop();

            //可选:判断当前节点是否合理(其实没必要,起点肯定一般都是合理,保证在下面扩展的时候不添加不合理的即可

            //判断是否达到终点
            if( cur_x == End.first && cur_y == End.second){
                found = true;
                break;
            }
            //扩展四个方向
            for(auto [dx, dy] : dirs){
                int newX = cur_x + dx;
                int newY = cur_y + dy;

                //判断合法
                if(isValid(newX, newY, n, m, grid, visited)){
                    q.push(make_tuple(newX, newY, steps+1));
                    visited[newX][newY] = true;
                    //记录父节点
                    parent[newX][newY] = make_pair(cur_x, cur_y);
                }
            }
        }
    }
    if(found){
        int x = End.first;
        int y = End.second;
        while(!(x == Start.first && y == Start.second)){
            grid[x][y] = 1;
            tie(x,y) = parent[x][y];
        }
        grid[Start.first][Start.second] = 1; //标记起点
    }
    return grid;
}

int main(){
    //输入
    int n, m;
    cout << "请输入电路板的行数和列数:";
    cin >> n >> m;

    vector <vector<int>> grid (n, vector<int>(m, 0));
    cout << "请输入电路版的状态(0为空,-1为封锁状态):"<<endl;
    for(int i = 0; i < n; i++){
        for(int j = 0; j < m; j++){
            cin >> grid[i][j];
        }
    }
    int startX, startY, endX, endY;
    cout << "请输入起点坐标(x y):";
    cin >> startX >> startY;
    cout << "请输入终点坐标(x y):";
    cin >> endX >> endY;

    //BFS遍历
    vector< vector<int> > res = shortestPath(grid, {startX, startY}, {endX, endY});

    //输出结果
    cout << "结果矩阵为:"<< endl;
    for(const auto& row : res){
        for(int cell : row){
            cout << cell << " ";
        }
        cout << endl;
    }

    return 0;
}

在这里插入图片描述

end:本文参考

🔗labuladongBFS算法解题套路框架

🔗布线问题分支界限法求解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1840613.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用SQLite

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 与许多其他数据库管理系统不同&#xff0c;SQLite不是一个客户端/服务器结构的数据库引擎&#xff0c;而是一种嵌入式数据库&#xff0c;它的数据库就…

微量氧传感器在3D打印中的应用

3D打印为什么需要监测氧气&#xff1f; 金属 3D 打印过程涉及使用激光技术将细金属粉末一层一层地熔合在一起。在制造零件的同时将杂质风险降至比较低是金属增材制造行业面临的主要挑战。金属 3D 打印机通常将其原料送入惰性环境中&#xff0c;以消除污染并防止质量问题。氩气…

【自撰写】【国际象棋入门】第6课 常见术语分析(一)吃双和抽将

第6课 常见术语分析&#xff08;一&#xff09;吃双和抽将 本次课中&#xff0c;我们介绍几种最为常见和常用的&#xff08;单步棋形成&#xff09;的局面、术语并对其进行简单的分析。一般说来&#xff0c;这些局面都会给予一方以“立竿见影”的优势&#xff0c;或者引向之后…

瑞尼克定制聚四氟乙烯布氏漏斗配抽滤瓶四氟抽滤装置药厂

一、产品介绍 布氏漏斗是实验室中使用的一种仪器&#xff0c;用来使用真空或负压力抽吸进行过滤。布氏漏斗可代替陶瓷布氏漏斗&#xff0c;避免碎裂&#xff0c;聚四氟乙烯材质的布氏漏斗性强&#xff0c;使用真空或负压力抽吸进行过滤也可与吸滤瓶配套&#xff0c;用于无机制…

window 卸载应用商店程序

# 使用Get-AppxPackage获取所有应用程序 # 使用Remove-AppxPackage PythonSoftwareFoundation.Python.3.12_3.12.1264.0_x64__qbz5n2kfra8p0

2024请收好这一份全面--详细的AI产品经理从业指南

前言 入行人工智能领域这段时间以来&#xff0c;从零到一把AI推荐系统产品化搭建了起来&#xff0c;也与很多同行AI产品经理小伙伴建立了联系。AI产品经理工作内容各异&#xff0c;不同AI产品化生命周期中更是大为不同&#xff0c;但对想入行AI产品经理的小伙伴来讲&#xff0…

聊一聊生成式AI

生成式AI&#xff08;Generative AI&#xff09;是指一类能够自主创造新内容的人工智能技术&#xff0c;这些内容可以是文本、图像、音频、视频等。与传统的分析性或分类性AI系统不同&#xff0c;生成式模型的主要任务不是对现有数据进行分类或预测&#xff0c;而是生成全新的、…

【C语言 || 排序】希尔排序

文章目录 前言1.希尔排序1.1 直接插入排序1.2 直接插入排序的实现1.2.1 直接插入排序的代码实现 1.3 直接插入排序的时间复杂度1.4 希尔排序1.4.1 希尔排序概念1.4.1 希尔排序的代码实现 前言 1.希尔排序 1.1 直接插入排序 在写希尔排序之前&#xff0c;我们需要先了解直接插入…

电压模式R-2R DAC的工作原理和特性

本文将探讨电压模式R-2R DAC结构。 在本文中&#xff0c;我们将探索什么是R-2R DAC以及如何实现它们。 首先&#xff0c;我们将简要回顾一下开尔文分压器DAC。这种结构很简单&#xff0c;但它们需要大量的电阻和开关来实现高分辨率DAC。这个问题的一个解决方案是称为R-2R DAC…

【python】用代码实现2024中科大强基计划数学科目第一题

题目&#xff1a; 已知正整数a,b,c满足10a11b12c123,&#xff0c;则&#xff08;a,b,c&#xff09;的组数是 思路&#xff1a; 为了找出满足等式 10a 11b 12c 123 的正整数三元组 (a, b, c) 的数量&#xff0c;我们可以使用Python编写一个简单的循环来遍历可能的 a、b 和…

哪种考勤机好用,常见好用的考勤机种类

哪种考勤机好用&#xff0c;常见好用的考勤机种类 用考勤机完成上下班打卡制度&#xff0c;极大地为人事对公司的管理提供了便利。不同种类的考勤机均有各自的长处&#xff0c;那么究竟哪种考勤机比较好用呢&#xff1f;其中&#xff0c;智能云考勤机能够实现异地手机打卡&…

推荐一个Python的前端框架Streamlit

WHY&#xff0c;为什么要用Streamlit 你是不是也想写一个简单的前端界面做些简单的展示和控制&#xff0c;不想写html、css、js&#xff0c;也用不到前后端分离&#xff0c;用不到特别复杂的Flask、Django等&#xff0c;如果你遇到类似这样的问题&#xff0c;我推荐你试试Stre…

Linux下调试代码——gdb的使用

1. 文件准备&#xff1a; 测试代码&#xff1a; Makefile文件&#xff1a; 执行结果&#xff1a; 此时&#xff0c;我们的结果是存在问题的&#xff0c;即最终结果少了100。现在我们用gdb来调试它。 我们发现我们还没有安装gdb&#xff0c;这里安装一下。 2. 环境准备&#…

CUDA系列-Mem-9

这里写目录标题 Static Architecture.Abstractions provided by CUSW_UNIT_MEM_MANAGERMemory Object (CUmemobj) Memory Descriptor(CUmemdesc)Memory Block(CUmemblock)Memory BinsSuballocations in Memory BlockFunctional description Memory Manager 你可能觉得奇怪&…

MacOS之解决:开盖启动问题(七十四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

LSTM架构的演进:LSTM、xLSTM、LSTM+Transformer

文章目录 1. LSTM2. xLSTM2.1 理论介绍2.2 代码实现 3. LSTMTransformer 1. LSTM 传统的 LSTM (长短期记忆网络) 的计算公式涉及几个关键部分&#xff1a;输入门、遗忘门、输出门和单元状态。 2. xLSTM xLSTM之所以称之为xLSTM就是因为它将LSTM扩展为多个LSTM的变体&#xff…

Spring的自动注入(也称为自动装配)

自动注入&#xff08;也称为自动装配&#xff09;是Spring框架中的一个核心概念&#xff0c;它与手动装配相对立&#xff0c;提供了一种更简洁、更灵活的方式来管理Bean之间的依赖关系。 在Spring应用程序中&#xff0c;如果类A依赖于类B&#xff0c;通常需要在类A中定义一个类…

终极版本的Typora上传到博客园和csdn

激活插件 下载网址是这个&#xff1a; https://codeload.github.com/obgnail/typora_plugin/zip/refs/tags/1.9.4 解压之后这样的&#xff1a; 解压之后将plugin&#xff0c;复制到自己的安装目录下的resources 点击安装即可&#xff1a; 更改配置文件 "dependencies&q…

SSMP整合案例

黑马程序员Spring Boot2 文章目录 1、创建项目1.1 新建项目1.2 整合 MyBatis Plus 2、创建表以及对应的实体类2.1 创建表2.2 创建实体类2.2.1 引入lombok&#xff0c;简化实体类开发2.2.2 开发实体类 3、数据层开发3.1 手动导入两个坐标3.2 配置数据源与MyBatisPlus对应的配置3…

第1讲:创建vite工程,使用框架为Vanilla时,语言是typescript,修改http端口的方法

直接在项目根目录创建 vite.config.ts文件。 在该文件中添加内容&#xff1a; import { defineConfig } from vite;export default defineConfig({server: {port: 7777,}, });最后尝试运行package.json中的Debug