引言
继续图论专题。
腐烂的橘子
- 🎈 题目链接:
- 🎈 做题状态:
我的解题
这道题应该使用 BFS 而不是 DFS ,因为每一层的时间(或者叫做步数)是需要一致的。如果是 DFS 的话每一层的步调不一致。
BFS 可以看成是层序遍历。从某个结点出发,BFS 首先遍历到距离为 1 的结点,然后是距离为 2、3、4…… 的结点。因此,BFS 可以用来求最短路径问题。BFS 先搜索到的结点,一定是距离最近的结点。
再看看这道题的题目要求:返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。翻译一下,实际上就是求腐烂橘子到所有新鲜橘子的最短路径。那么这道题使用 BFS,应该是毫无疑问的了。
这道题的主要思路是:
- 一开始,我们找出所有腐烂的橘子,将它们放入队列,作为第 0 层的结点。
- 然后进行 BFS 遍历,每个结点的相邻结点可能是上、下、左、右四个方向的结点,注意判断结点位于网格边界的特殊情况。
- 由于可能存在无法被污染的橘子,我们需要记录新鲜橘子的数量。在 BFS 中,每遍历到一个橘子(污染了一个橘子),就将新鲜橘子的数量减一。如果 BFS 结束后这个数量仍未减为零,说明存在无法被污染的橘子。
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
queue<pair<int, int>> q;
int count = 0; // 记录新鲜橘子的数量
// 遍历格网,将所有的烂橘子作为第一层加入到队列中
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
++count;
} else if (grid[i][j] == 2) {
q.push({i, j});
}
}
}
int round = 0; // 表示水果腐烂的轮数
// 定义四个方向的偏移量:上、下、左、右
vector<pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!q.empty() && count > 0) {
++round;
int size = q.size(); // 记录当前层所有腐败的水果
// 弹出当前层所有的腐败水果
for (int i = 0; i < size; ++i) {
auto [x, y] = q.front(); q.pop();
for (auto [dx, dy] : directions) {
int nx = x + dx;
int ny = y + dy;
// 检查新坐标是否在网格范围内且是新鲜橘子
if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
grid[nx][ny] = 2;
--count;
q.push({nx, ny});
}
}
}
}
// 判断腐败完是否还有新鲜水果
if (count > 0)
{
return -1;
} else
{
return round;
}
}
};
BFS 和 DFS 的适用场景及选择策略
1. BFS(广度优先搜索)
核心思想:按“层”遍历,先访问离起点最近的节点,再逐步向外扩展。
数据结构:队列(FIFO)。
适用场景:
- 最短路径问题(无权图或所有边权重相同):
- 如“腐烂的橘子”、“迷宫的最短路径”、“单词接龙”等。
- 因为 BFS 保证第一次到达目标时的路径一定是最短的。
- 分层遍历或按距离处理:
- 如“二叉树的层序遍历”、“岛屿数量”等需要逐层扩散的问题。
- 拓扑排序:
- 如“课程表”等依赖关系问题,可以用 BFS(Kahn 算法)实现。
- 状态空间搜索(步数最小化):
- 如“滑动谜题”、“八数码问题”等需要最少步数的问题。
不适用场景:
- 问题不需要最短路径,且图/树深度很大(可能导致队列内存爆炸)。
- 需要递归或回溯的问题(如全排列、组合问题)。
2. DFS(深度优先搜索)
核心思想:一条路走到底,回溯后再尝试其他路径。
数据结构:栈(递归或显式栈)。
适用场景:
- 连通性问题:
- 如“岛屿的最大面积”、“图的连通分量”等,DFS 可以轻松标记所有连通区域。
- 回溯问题:
- 如“全排列”、“组合总和”、“N 皇后”等需要尝试所有可能解的问题。
- 拓扑排序:
- DFS 也可以实现(通过后序遍历反转)。
- 路径问题(不关心最短路径):
- 如“二叉树的所有路径”、“图的简单路径”等。
不适用场景:
- 需要最短路径的问题(DFS 可能绕远路)。
- 图/树非常宽(递归栈可能溢出)。
3. 如何选择 BFS 或 DFS?
问题特征 | 选择 BFS | 选择 DFS |
---|---|---|
需要最短路径/最小步数 | ✅(如迷宫、单词接龙) | ❌(可能绕路) |
需要遍历所有可能解 | ❌(队列内存消耗大) | ✅(如全排列、组合问题) |
图/树深度很大 | ❌(队列可能爆内存) | ✅(递归栈可控) |
图/树宽度很大 | ✅(按层处理) | ❌(递归栈可能溢出) |
需要快速找到一个解 | ✅(首次到达即最优) | ✅(可能更快碰运气) |
需要记录路径 | ❌(需额外存储路径) | ✅(回溯天然记录路径) |
4. 经典例题对比
题目 | BFS 解法 | DFS 解法 |
---|---|---|
二叉树层序遍历 | ✅ 天然分层遍历 | ❌ 需记录深度,不如 BFS 直观 |
岛屿数量 | ✅ 逐层扩散标记 | ✅ 递归标记连通区域 |
单词接龙(最短转换序列) | ✅ 最短路径 | ❌ 可能超时 |
全排列 | ❌ 需维护状态队列,复杂 | ✅ 回溯天然适合 |
腐烂的橘子 | ✅ 最短时间扩散 | ❌ 无法保证最短时间 |
5. 总结
- 用 BFS 如果:
- 需要最短路径、分层处理、状态空间步数最小化。
- 问题类似“扩散”、“最少步数”、“层序遍历”。
- 用 DFS 如果:
- 需要递归/回溯、遍历所有解、连通性分析。
- 问题类似“所有可能”、“连通区域”、“排列组合”。
灵活结合:某些问题可以同时用 BFS 和 DFS(如“岛屿数量”),但要根据问题特点选择更优解。