一、BFS适用最短路径问题的条件
-
无权图(或边权均相同)
BFS天然保证首次访问某节点时的路径就是最短路径(因为按层扩展)。典型场景:迷宫最短步数、社交网络中的“最少中间人”。 -
图的边权非负且相等
如果边权均为同一正数(如均视为1),BFS仍有效。 -
需要显式遍历所有可能状态
适用于状态空间明确且可逐层展开的问题(如八数码问题)。
二、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的核心优势
-
无需复杂数据结构:仅需队列和访问标记。
-
保证最优性:在适用场景下首次访问即最短路径。
-
编码直观:模板化强,易调试
四、使用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.解题思路
-
初始化:
-
使用一个队列(
queue
)来存储待处理的基因序列。 -
使用一个集合(
unordered_set
)来存储已经搜索过的基因序列,避免重复搜索。 -
使用一个字符串(
change
)来存储可能的变异字符。
-
-
广度优先搜索:
-
从起始基因序列(
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
中的单词,并且每个单词只能使用一次。
-
验证可行性:
-
首先检查
endWord
是否在wordList
中,如果不在,则无法形成路径,直接返回 0。
-
-
图的构建:
-
将问题建模为图论问题,其中
beginWord
为起点,endWord
为终点,wordList
中的每个单词为图中的节点。 -
边的定义为:如果两个单词之间只有一个字母不同,则它们之间存在一条边。
-
-
广度优先搜索(BFS):
-
使用队列实现 BFS 算法来找到最短路径。
-
使用
unordered_set
visi
来记录已经访问过的单词,避免重复访问。 -
使用
unordered_set
hash
来快速检查单词是否在wordList
中。
-
-
路径搜索:
-
从
beginWord
开始,将其加入队列和visi
。 -
当队列不为空时,重复以下步骤:
-
扩展队列中的每个单词,对于每个单词,尝试所有可能的一步变化(即改变一个字母)。
-
如果变化后的单词在
wordList
中且未被访问过,将其加入队列和visi
。 -
如果变化后的单词是
endWord
,则返回当前步数。
-
-
重复上述步骤直到找到目标单词或队列为空。
-
-
返回结果:
-
如果找到路径,返回步数
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;}};