文章目录
-
- [一、 核心原理:为什么最短路首选 BFS?](#一、 核心原理:为什么最短路首选 BFS?)
- [二、 迷宫中离入口最近的出口 (Medium)](#二、 迷宫中离入口最近的出口 (Medium))
-
- [2.1 题目描述](#2.1 题目描述)
- [2.2 深度拆解:层序遍历的步数控制](#2.2 深度拆解:层序遍历的步数控制)
- [2.3 C++ 代码实战](#2.3 C++ 代码实战)
- [三、 最小基因变化 (Medium)](#三、 最小基因变化 (Medium))
-
- [3.1 题目描述](#3.1 题目描述)
- [3.2 深度拆解:字符串作为"坐标"](#3.2 深度拆解:字符串作为“坐标”)
- [3.3 C++ 代码实战](#3.3 C++ 代码实战)
- [四、 单词接龙 (Hard)](#四、 单词接龙 (Hard))
-
- [4.1 题目描述](#4.1 题目描述)
- [4.2 深度拆解:为何是 Hard?](#4.2 深度拆解:为何是 Hard?)
- [4.3 C++ 代码实战](#4.3 C++ 代码实战)
- [五、 为高尔夫比赛砍树 (Hard)](#五、 为高尔夫比赛砍树 (Hard))
-
- [5.1 题目描述](#5.1 题目描述)
- [5.2 深度拆解:最短路的叠加](#5.2 深度拆解:最短路的叠加)
- [5.3 C++ 代码实战](#5.3 C++ 代码实战)
- [六、 总结与避坑](#六、 总结与避坑)
一、 核心原理:为什么最短路首选 BFS?
💬 底层逻辑 :
BFS(广度优先搜索)就像在水面上丢一颗石子,涟漪是均匀地、一圈一圈地向外扩散的。
- 等权性:在棋盘、迷宫或变换步数为 1 的图中,每一层扩散代表步数 +1。
- 最优性证明 :假设目标点在第 k k k 层。由于 BFS 严格按照层级搜索,在搜到第 k k k 层之前,所有的 1 , 2 , ... , k − 1 1, 2, \dots, k-1 1,2,...,k−1 层都已经搜过了且没找到目标。因此,第一次接触到目标点时,所经过的路径长度必定是最小的。
🚀 建模三步走:
- 确定状态(点) :坐标
(x, y)、基因序列string、单词string。- 确定转移(边):上下左右走一步、变一个字符。
- 确定边界:越界判断、是否在基因库/字典中、是否已访问(防止死循环)。
二、 迷宫中离入口最近的出口 (Medium)
2.1 题目描述
给定一个
m x n的迷宫,.是空地,+是墙。从entrance出发,找到离入口最近的边界空地(出口),返回最少步数。入口本身不算出口。
2.2 深度拆解:层序遍历的步数控制
- 搜索起点:入口坐标。
- 搜索终点 :坐标在第一行、最后一行、第一列或最后一列,且不是入口点。
- 层序计数技巧 :
在while(!q.empty())内部,先获取int sz = q.size(),一次性处理完这sz个节点。这sz个节点属于同一"层"(即步数相同)。这样,我们只需在处理完一层后step++即可。
2.3 C++ 代码实战
cpp
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();
// 1. 记录访问状态,防止走回头路
vector<vector<bool>> vis(m, vector<bool>(n, false));
queue<pair<int, int>> q;
q.push({entrance[0], entrance[1]});
vis[entrance[0]][entrance[1]] = true;
int step = 0;
while (!q.empty()) {
step++; // 准备扩散到下一层
int sz = q.size(); // 锁定当前层节点数
for (int i = 0; i < sz; i++) {
auto [a, b] = q.front(); q.pop();
for (int j = 0; j < 4; j++) {
int x = a + dx[j], y = b + dy[j];
// 2. 检查:不越界 && 是通路 && 没走过
if (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == '.' && !vis[x][y]) {
// 3. 判断是否为出口:在边缘上
if (x == 0 || x == m - 1 || y == 0 || y == n - 1) return step;
vis[x][y] = true;
q.push({x, y});
}
}
}
}
return -1; // 搜遍全图也出不去
}
};
三、 最小基因变化 (Medium)
3.1 题目描述
链接 :433. 最小基因变化
8个字符组成的基因。一次变化改变一个字符。必须在
bank中的变化才有效。求从start到end的最小变化次数。
3.2 深度拆解:字符串作为"坐标"
这题的本质是在字符串组成的抽象图中找最短路。
- 状态转移 :对于当前基因的 8 个位置,每个位置尝试替换为
A, C, G, T4 种。 - 合法性判定 :变换后的字符串必须出现在
bank集合中。 - 性能优化 :将
bank转化为unordered_set,查询时间从 O ( N ) O(N) O(N) 降到 O ( 1 ) O(1) O(1)。
3.3 C++ 代码实战
cpp
class Solution {
public:
int minMutation(string start, string end, vector<string>& bank) {
unordered_set<string> dict(bank.begin(), bank.end());
if (!dict.count(end)) return -1; // 终点不合法,直接判死刑
unordered_set<string> vis; // 字符串版本的 vis
queue<string> q;
q.push(start);
vis.insert(start);
int step = 0;
string choice = "ACGT";
while (!q.empty()) {
int sz = q.size();
while (sz--) {
string curr = q.front(); q.pop();
if (curr == end) return step;
// 核心:尝试改变 8 个位置中的每一个
for (int i = 0; i < 8; i++) {
char oldChar = curr[i];
for (char ch : choice) {
curr[i] = ch; // 模拟变换
// 如果变换后的基因合法且未访问
if (dict.count(curr) && !vis.count(curr)) {
vis.insert(curr);
q.push(curr);
}
}
curr[i] = oldChar; // 状态恢复,准备尝试下一个位置
}
}
step++;
}
return -1;
}
};
四、 单词接龙 (Hard)
4.1 题目描述
链接 :127. 单词接龙
找到从
beginWord到endWord的最短转换序列中的 单词数目。
4.2 深度拆解:为何是 Hard?
与"基因变化"逻辑一致,但难点在于:
- 返回值 :要求的是"单词数目",即
步数 + 1。 - 数据规模 :
wordList可能很大。如果对每个单词去和 List 里的其他单词比(看是否只差一个字母),时间复杂度 O ( N 2 × L ) O(N^2 \times L) O(N2×L)。 - 优化策略 :依然采取 "改变当前单词的每一位" ,复杂度降为 O ( N × L × 26 ) O(N \times L \times 26) O(N×L×26),在 L L L 很小时效果极佳。
4.3 C++ 代码实战
cpp
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> dict(wordList.begin(), wordList.end());
if (!dict.count(endWord)) return 0;
queue<string> q;
q.push(beginWord);
// 初始单词也算一个,所以 step 从 1 开始
int step = 1;
while (!q.empty()) {
int sz = q.size();
while (sz--) {
string word = q.front(); q.pop();
if (word == endWord) return step;
// 尝试修改每一位字母
for (int i = 0; i < word.size(); i++) {
char original = word[i];
for (char c = 'a'; c <= 'z'; c++) {
word[i] = c;
if (dict.count(word)) {
q.push(word);
dict.erase(word); // 优化:erase 相当于标记已访问
}
}
word[i] = original;
}
}
step++;
}
return 0;
}
};
五、 为高尔夫比赛砍树 (Hard)
5.1 题目描述
链接 :675. 为高尔夫比赛砍树
必须按照树的高度从小到大顺序砍掉所有树。求总步数。
5.2 深度拆解:最短路的叠加
这题的 Hard 在于它是多次最短路求和:
-
预处理 :扫描矩阵,提取所有树的坐标,按高度
sort。 -
串联搜索:
- 第 1 阶段:从
(0, 0)走到高度第 1 矮的树。 - 第 2 阶段:从第 1 矮的树走到第 2 矮的树。
- ...
- 第 1 阶段:从
-
结果累加:每一次移动都是一次独立的 BFS,如果中途任何一个 BFS 失败(返回 -1),整个任务直接失败。
5.3 C++ 代码实战
cpp
class Solution {
int m, n;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
public:
int cutOffTree(vector<vector<int>>& forest) {
m = forest.size(); n = forest[0].size();
vector<pair<int, int>> trees;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (forest[i][j] > 1) trees.push_back({i, j});
}
}
// 1. 按高度排序
sort(trees.begin(), trees.end(), [&](pair<int,int>& a, pair<int,int>& b){
return forest[a.first][a.second] < forest[b.first][b.second];
});
// 2. 依次计算每两棵树之间的最短距离
int startX = 0, startY = 0;
int totalSteps = 0;
for (auto& tree : trees) {
int d = bfs(forest, startX, startY, tree.first, tree.second);
if (d == -1) return -1; // 无法到达
totalSteps += d;
startX = tree.first; startY = tree.second; // 砍完这棵,这就是新起点
}
return totalSteps;
}
int bfs(vector<vector<int>>& f, int sx, int sy, int tx, int ty) {
if (sx == tx && sy == ty) return 0;
queue<pair<int, int>> q;
vector<vector<bool>> vis(m, vector<bool>(n, false));
q.push({sx, sy});
vis[sx][sy] = true;
int step = 0;
while (!q.empty()) {
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] > 0 && !vis[x][y]) {
if (x == tx && y == ty) return step;
vis[x][y] = true;
q.push({x, y});
}
}
}
}
return -1;
}
};
六、 总结与避坑
💬 复盘:BFS 最短路是模板化程度非常高的算法。
- 步数同步 :一定要用
sz = q.size()锁定当前层,否则步数统计会乱。 - 提前返回 :在
push下一层节点时直接判断是否到达目标,比弹出时再判断快一倍。 - 空间换时间 :
unordered_set和vis数组是搜索的铁律。