当前位置: 首页 > news >正文

BFS解决最短路径问题(使用BFS解决最短路径问题的黄金法则)

一、BFS适用最短路径问题的条件

  1. 无权图(或边权均相同)
    BFS天然保证首次访问某节点时的路径就是最短路径(因为按层扩展)。典型场景:迷宫最短步数、社交网络中的“最少中间人”。

  2. 图的边权非负且相等
    如果边权均为同一正数(如均视为1),BFS仍有效。

  3. 需要显式遍历所有可能状态
    适用于状态空间明确且可逐层展开的问题(如八数码问题)。

二、BFS vs 其他最短路径算法对比

算法适用场景时间复杂度空间复杂度
BFS无权图或等权图O(V + E)O(V)
Dijkstra带权图(无负权边)O((V+E)logV)O(V)
Bellman-Ford带权图(允许负权边)O(VE)O(V)
A*带权图+启发式搜索(如地图导航)取决于启发函数O(V)

三、BFS的核心优势

  1. 无需复杂数据结构:仅需队列和访问标记。

  2. 保证最优性:在适用场景下首次访问即最短路径。

  3. 编码直观:模板化强,易调试

 四、使用BFS解决最短路径问题的黄金法则


✅ 用BFS:无权图/等权图、状态转移代价相同、需要最少步数/层级的问题。
❌ 换算法:带权图(除非权值统一)、存在负权边、超大状态空间。

五、相关例题

1.迷宫中离入口最近的出口 

​​​​​​1926. 迷宫中离入口最近的出口 - 力扣(LeetCode)

1.题目解析

给你一个 m x n 的迷宫矩阵 maze (下标从 0 开始),矩阵中有空格子(用 '.' 表示)和墙(用 '+' 表示)。同时给你迷宫的入口 entrance ,用 entrance = [entrancerow, entrancecol] 表示你一开始所在格子的行和列。

每一步操作,你可以往  或者  移动一个格子。你不能进入墙所在的格子,你也不能离开迷宫。你的目标是找到离 entrance 最近 的出口。出口 的含义是 maze 边界 上的 空格子entrance 格子 不算 出口。

请你返回从 entrance 到最近出口的最短路径的 步数 ,如果不存在这样的路径,请你返回 -1 。

2.示例

示例 1:

输入:maze = [["+","+",".","+"],[".",".",".","+"],["+","+","+","."]], entrance = [1,2]
输出:1
解释:总共有 3 个出口,分别位于 (1,0),(0,2) 和 (2,3) 。
一开始,你在入口格子 (1,2) 处。
- 你可以往左移动 2 步到达 (1,0) 。
- 你可以往上移动 1 步到达 (0,2) 。
从入口处没法到达 (2,3) 。
所以,最近的出口是 (0,2) ,距离为 1 步。

示例 2:

输入:maze = [["+","+","+"],[".",".","."],["+","+","+"]], entrance = [1,0]
输出:2
解释:迷宫中只有 1 个出口,在 (1,2) 处。
(1,0) 不算出口,因为它是入口格子。
初始时,你在入口与格子 (1,0) 处。
- 你可以往右移动 2 步到达 (1,2) 处。
所以,最近的出口为 (1,2) ,距离为 2 步。

示例 3:

输入:maze = [[".","+"]], entrance = [0,0]
输出:-1
解释:这个迷宫中没有出口。

3.解题思路

1.注意:本题不能使用bool visi[m][n],会导致编译出错

应该使用vector<vector<bool>> visi(m, vector<bool>(n, false))

利⽤层序遍历来解决迷宫问题,是最经典的做法。 我们可以从起点开始层序遍历,并且在遍历的过程中记录当前遍历的层数。这样就能在找到出⼝的 时候,得到起点到出⼝的最短距离

4.代码实现

class Solution {
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};
public:
    int nearestExit(vector<vector<char>>& maze, vector<int>& entrance) {
        int m = maze.size(), n = maze[0].size();
        queue<pair<int, int>> q;
        vector<vector<bool>> visi(m, vector<bool>(n, false));
        q.push({entrance[0],entrance[1]});
        visi[entrance[0]][entrance[1]] = true;
        int step = 0;

        while(q.size())
        {
            int sz = q.size();
            step++;
            for(int i = 0; i < sz; i++)
            {
                auto [a, b] = q.front();
                q.pop();
            
                for(int i = 0; i < 4; i++)
                {
                    int x = a + dx[i], y = b + dy[i];
                    if(x >=0 && x < m && y >= 0 && y < n && !visi[x][y] && maze[x][y] == '.')
                    {
                        if(x == 0 || x == m - 1 || y == 0 || y == n - 1)
                            return  step;
                        q.push({x, y});
                        visi[x][y] = true;
                    }
                }
            }
        }
        return -1;
    }
};

2.最小基因变化

433. 最小基因变化 - 力扣(LeetCode)

1.题目解析

基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A''C''G' 和 'T' 之一。

假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。

  • 例如,"AACCGGTT" --> "AACCGGTA" 就是一次基因变化。

另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank 中)

给你两个基因序列 start 和 end ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。

注意:起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。

2.示例

示例 1:

输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]
输出:1

示例 2:

输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]
输出:2

示例 3:

输入:start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"]
输出:3

3.解题思路

  1. 初始化

    • 使用一个队列(queue)来存储待处理的基因序列。

    • 使用一个集合(unordered_set)来存储已经搜索过的基因序列,避免重复搜索。

    • 使用一个字符串(change)来存储可能的变异字符。

  2. 广度优先搜索

    • 从起始基因序列(startGene)开始,将其加入队列。

    • 当队列不为空时,重复以下步骤:

      • 计算当前队列的大小(sz),表示当前层的基因序列数量。

      • 创建一个临时列表(tmp)来存储当前层的变异结果。

      • 遍历当前层的每个基因序列,对于每个序列:

        • 遍历每个可能的变异点,尝试用 change 字符串中的字符替换当前字符。

        • 生成新的基因序列(tmp)。

        • 检查新生成的基因序列是否在基因库(bank)中且未被访问过。

        • 如果是,且等于目标基因序列(endGene),则返回当前的变异次数。

        • 否则,将新序列加入队列,标记为已访问。

      • 增加变异次数(ret+++)。

4.代码实现

class Solution {
public:
    int minMutation(string startGene, string endGene, vector<string>& bank) {
        unordered_set<string> vis;//用来标记已经搜索过的状态
        unordered_set<string> hash(bank.begin(), bank.end());//用来标记基因库中出现的基因模拟
        string change = "ACGT";

        if(startGene == endGene) return 0;
        if(!hash.count(endGene)) return -1;

        queue<string> q;
        int ret = 0;
        q.push(startGene);
        vis.insert(startGene);
        while(q.size())
        {
            ret++;
            int sz = q.size();
            while(sz--)
            {
                string t = q.front();
                q.pop();
                for(int i = 0; i < 8; i++)
                {
                    string tmp = t;
                    for(int j = 0; j < 4; j++)
                    {
                        tmp[i] = change[j];
                        if(hash.count(tmp) && !vis.count(tmp))
                        {
                            if( tmp == endGene) return ret;
                            q.push(tmp);
                            vis.insert(tmp);              
                        }
                    }
                }
            }
        }
        return -1;
        
    }
};

3.单词接龙

127. 单词接龙 - 力扣(LeetCode)

1.题目解析

字典 wordList 中从单词 beginWord 到 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  •  对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
  • sk == endWord

给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。

2.示例

示例 1:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。

示例 2:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
输出:0
解释:endWord "cog" 不在字典中,所以无法进行转换。

3.解题思路

        这道题目是经典的 "单词梯子" 问题,目标是找到从给定的起始单词 beginWord 到目标单词 endWord 的最短路径长度,路径上的每一步都必须是 wordList 中的单词,并且每个单词只能使用一次。

  1. 验证可行性

    • 首先检查 endWord 是否在 wordList 中,如果不在,则无法形成路径,直接返回 0。

  2. 图的构建

    • 将问题建模为图论问题,其中 beginWord 为起点,endWord 为终点,wordList 中的每个单词为图中的节点。

    • 边的定义为:如果两个单词之间只有一个字母不同,则它们之间存在一条边。

  3. 广度优先搜索(BFS)

    • 使用队列实现 BFS 算法来找到最短路径。

    • 使用 unordered_set visi 来记录已经访问过的单词,避免重复访问。

    • 使用 unordered_set hash 来快速检查单词是否在 wordList 中。

  4. 路径搜索

    • beginWord 开始,将其加入队列和 visi

    • 当队列不为空时,重复以下步骤:

      • 扩展队列中的每个单词,对于每个单词,尝试所有可能的一步变化(即改变一个字母)。

      • 如果变化后的单词在 wordList 中且未被访问过,将其加入队列和 visi

      • 如果变化后的单词是 endWord,则返回当前步数。

    • 重复上述步骤直到找到目标单词或队列为空。

  5. 返回结果

    • 如果找到路径,返回步数 step

    • 如果未找到路径,返回 0。

4.代码实现

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        unordered_set<string> visi;//标记出现过的字符串
        unordered_set<string> hash(wordList.begin(), wordList.end());//记录wordList里出现的单词
        string change = "abcdefghijklmnopqrstuvwxyz";
        int step = 1;

        if(!hash.count(endWord)) return 0;

        queue<string> q;
        q.push(beginWord);
        visi.insert(beginWord);

        while(q.size())
        {
            int sz = q.size();
            step++;
            while(sz--)
            {
                string t = q.front();
                q.pop();
                for(int i = 0; i < t.size(); i++)
                {
                    string tmp = t;
                    for(int j = 0; j < 26; j++)
                    {
                        tmp[i] = change[j];
                        if(hash.count(tmp) && !visi.count(tmp))
                        {
                            if(tmp == endWord)
                                return step;
                            q.push(tmp);
                            visi.insert(tmp);
                        }
                    }
                }
            }


        }
        return 0;
    }
};

4.为高尔夫比赛砍树

675. 为高尔夫比赛砍树 - 力扣(LeetCode)

1.题目解析

你被请来给一个要举办高尔夫比赛的树林砍树。树林由一个 m x n 的矩阵表示, 在这个矩阵中:

  • 0 表示障碍,无法触碰
  • 1 表示地面,可以行走
  • 比 1 大的数 表示有树的单元格,可以行走,数值表示树的高度

每一步,你都可以向上、下、左、右四个方向之一移动一个单位,如果你站的地方有一棵树,那么你可以决定是否要砍倒它。

你需要按照树的高度从低向高砍掉所有的树,每砍过一颗树,该单元格的值变为 1(即变为地面)。

你将从 (0, 0) 点开始工作,返回你砍完所有树需要走的最小步数。 如果你无法砍完所有的树,返回 -1 。

可以保证的是,没有两棵树的高度是相同的,并且你至少需要砍倒一棵树。

2.示例

示例 1:

输入:forest = [[1,2,3],[0,0,4],[7,6,5]]
输出:6
解释:沿着上面的路径,你可以用 6 步,按从最矮到最高的顺序砍掉这些树。

示例 2:

输入:forest = [[1,2,3],[0,0,0],[7,6,5]]
输出:-1
解释:由于中间一行被障碍阻塞,无法访问最下面一行中的树。

示例 3:

输入:forest = [[2,3,4],[0,0,5],[8,7,6]]
输出:6
解释:可以按与示例 1 相同的路径来砍掉所有的树。
(0,0) 位置的树,可以直接砍去,不用算步数。

3.解题思路

 问题可以转换成,从一个数到另一个数的最小距离(若干个迷宫问题)

4.代码实现

class Solution 
{
    int m, n;
 public:
    int cutOffTree(vector<vector<int>>& f) 
    {
        m = f.size(), n = f[0].size();
        // 1. 准备⼯作:找出砍树的顺序
 
        vector<pair<int, int>> trees;
        for(int i = 0; i < m; i++)
            for(int j = 0; j < n; j++)
                if(f[i][j] > 1) trees.push_back({i, j});
        
        sort(trees.begin(), trees.end(), [&](const pair<int, int>& p1, const 
pair<int, int>& p2)
        {
            return f[p1.first][p1.second] < f[p2.first][p2.second];
        });
        // 2. 按照顺序砍树
 
        int bx = 0, by = 0;
        int ret = 0;
        for(auto& [a, b] : trees)
        {
            int step = bfs(f, bx, by, a, b);
            if(step == -1) return -1;
            ret += step;
            bx = a, by = b;
        }
        return ret;
    }
    bool vis[51][51];
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};
    int bfs(vector<vector<int>>& f, int bx, int by, int ex, int ey)
    {
        if(bx == ex && by == ey) return 0;
        queue<pair<int, int>> q;
        memset(vis, 0, sizeof vis); // 清空之前的数据
 
        q.push({bx, by});
        vis[bx][by] = true;
        int step = 0;
        while(q.size())
        {
            step++;
            int sz = q.size();
            while(sz--)
            {
                auto [a, b] = q.front();
                q.pop();
                for(int i = 0; i < 4; i++)
                {
                    int x = a + dx[i], y = b + dy[i];
                    if(x >= 0 && x < m && y >= 0 && y < n && f[x][y] && !vis[x][y])
                    {
                        if(x == ex && y == ey) return step;
                        q.push({x, y});
                        vis[x][y] = true;
                    }
                }
            }
        }
        return -1;
    }
 };

相关文章:

  • Python生成整数序列之 range()函数
  • 《C++11:bind绑定器与function包装器》
  • Kotlin when 表达式完全指南:从基础到高级的12种实战用法
  • Redis的深入了解
  • 页面类buttons权限的简单控制
  • MySQL学习日志--多表查询篇
  • NDK CMake工程中引入其他C++三方库
  • 视频格式转换:畅享多平台无缝视频体验
  • 31天Python入门——第16天:模块与库详解
  • StarRocks语句解析json字符串的方法
  • 还刷刷刷刷刷
  • 洛谷1044栈C语言
  • 2025Java面试TOP1000问:源码级解答+避坑指南+性能优化
  • [C++][STL]unordered_set类和unordered_map类
  • 【MySQL】从零开始:掌握MySQL数据库的核心概念(五)
  • columns实现瀑布流布局
  • ⑦(ACG-网络配置)
  • 06 单目标定-去畸变
  • React Native与原生Android数据传递
  • 【Unity】 鼠标拖动物体移动速度跟不上鼠标,会掉落
  • 网站建设公司哪家好/seo排名第一的企业
  • 手机免费制作ppt/陕西网络营销优化公司
  • 网站建设与推广实训总结/星巴克营销策划方案
  • 做美女图片网站犯法吗/app开发流程
  • 怎么做属于自己的领券网站/推广优化厂商联系方式
  • 网站后台模板/武汉seo软件