1. 问题背景与算法选择
腐烂橘子问题是LeetCode上经典的图遍历问题(编号994),题目描述如下:在一个给定的网格中,每个单元格可以有以下三种状态之一:
- 0 表示空单元格
- 1 表示新鲜橘子
- 2 表示腐烂橘子
每分钟,任何与腐烂橘子相邻(上下左右)的新鲜橘子都会腐烂。我们需要计算直到没有新鲜橘子被腐烂为止所需的最小分钟数,如果不可能则返回-1。
这个问题之所以被归入LeetCode热题100,是因为它完美展现了多源BFS(广度优先搜索)的应用场景。与单源BFS从单一起点扩散不同,多源BFS需要同时从多个起点开始扩散,这在处理并行腐烂过程时尤为关键。
提示:理解多源BFS的关键在于认识到所有初始腐烂橘子相当于同时开始"感染"周围橘子,这与流行病学中的多点爆发传播模型高度相似。
2. 算法设计与实现思路
2.1 基础BFS框架的调整
传统BFS使用单个队列和单个起点,而多源BFS需要:
- 初始化时将所有腐烂橘子坐标加入队列
- 为每个橘子记录其腐烂时间(即BFS的层数)
- 使用一个计数器跟踪剩余新鲜橘子数量
Java实现时需要特别注意:
- 使用Queue接口的具体实现(通常选择LinkedList)
- 自定义一个包含坐标和时间的内部类或使用数组
- 方向数组表示四个移动方向(上右下左)
java复制// 方向数组示例
int[][] dirs = {{-1,0}, {0,1}, {1,0}, {0,-1}}; // 上、右、下、左
2.2 时间计算的特殊处理
每分钟所有腐烂橘子会同时影响相邻格子,这导致时间计算的两个关键点:
- 同一"层"的所有腐烂处理属于同一时间单位
- 需要区分"没有新鲜橘子"和"永远有无法腐烂的橘子"两种情况
在代码实现中,我们采用:
- 每处理完一层(即队列中当前所有元素)时间+1
- 最后检查剩余新鲜橘子数量决定返回-1还是总时间
3. 完整Java实现与逐行解析
3.1 类结构与初始化
java复制class Solution {
public int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int fresh = 0;
// 初始化:统计新鲜橘子,腐烂橘子入队
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) fresh++;
else if (grid[i][j] == 2) queue.offer(new int[]{i, j});
}
}
// 边界情况处理
if (fresh == 0) return 0;
if (queue.isEmpty()) return -1;
// 后续代码...
}
}
3.2 BFS核心逻辑
java复制int time = -1; // 初始化为-1因为第一次循环会+1
while (!queue.isEmpty()) {
int size = queue.size();
time++;
for (int i = 0; i < size; i++) {
int[] cur = queue.poll();
for (int[] dir : dirs) {
int x = cur[0] + dir[0];
int y = cur[1] + dir[1];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1) {
grid[x][y] = 2;
fresh--;
queue.offer(new int[]{x, y});
}
}
}
}
return fresh == 0 ? time : -1;
3.3 关键点说明
- 时间初始化-1的技巧:因为第一次循环处理的是初始已腐烂的橘子,它们不需要时间传播
- 分层处理:通过queue.size()获取当前层元素数量
- 新鲜橘子实时计数:避免最后再次遍历整个网格
4. 复杂度分析与优化空间
4.1 时间复杂度
- 最坏情况下需要遍历整个网格两次(初始统计+BFS)
- 时间复杂度为O(m×n),其中m和n分别是网格的行列数
4.2 空间复杂度
- 队列最大可能存储所有格子(当全部橘子同时腐烂)
- 空间复杂度为O(m×n)
4.3 可能的优化方向
- 原地修改:利用输入数组本身标记状态,无需额外空间
- 多端BFS:从多个腐烂源同时扩散(本解法已实现)
- 提前终止:当新鲜橘子数为0时可立即返回
5. 边界条件与测试案例
5.1 必须考虑的边界情况
- 网格为空或只有一行/一列
- 初始没有新鲜橘子
- 初始没有腐烂橘子但存在新鲜橘子
- 存在永远无法被腐烂的新鲜橘子(被0隔离)
5.2 测试案例示例
java复制// 案例1:正常情况
int[][] grid1 = {{2,1,1},{1,1,0},{0,1,1}};
// 应返回4
// 案例2:无法完全腐烂
int[][] grid2 = {{2,1,1},{0,1,1},{1,0,1}};
// 应返回-1
// 案例3:初始无新鲜橘子
int[][] grid3 = {{0,2}};
// 应返回0
6. 算法变体与实际应用
6.1 变体问题
- 加权版本:不同方向的腐烂速度不同
- 三维版本:考虑立体空间中的腐烂传播
- 概率版本:每次传播有一定失败概率
6.2 实际应用场景
- 网络传播模型:信息/病毒在多节点间的扩散
- 火灾蔓延模拟:多点起火的消防策略制定
- 自动化测试:从多个入口点同时探索状态空间
7. 常见错误与调试技巧
7.1 典型错误模式
- 忘记处理初始无新鲜橘子的情况
- 时间计算错误(特别是初始化为0而不是-1)
- 没有及时更新网格状态导致重复处理
- 方向数组定义不全(漏掉某个方向)
7.2 调试建议
- 打印每步后的网格状态
- 跟踪队列大小和时间变化
- 使用小规模测试案例手动验证
注意:在面试中建议先口头说明这些检查点,展示严谨的思维过程
8. 与其他算法的对比
8.1 与DFS的适用性对比
DFS不适合此场景的原因:
- 需要最短时间(BFS天然按层扩散)
- 并行处理多个源头更复杂
- 可能陷入深层递归
8.2 与Dijkstra算法的关系
当引入传播权重时:
- 多源BFS可看作所有权重相等时的Dijkstra特例
- 优先级队列会取代普通队列
9. 代码风格与工程实践
9.1 可读性优化
- 使用枚举常量代替魔法数字:
java复制final int EMPTY = 0, FRESH = 1, ROTTEN = 2;
- 提取方向数组为静态常量
- 为复杂逻辑添加注释
9.2 单元测试建议
- 测试应包括:
- 最小网格(1x1)
- 全腐烂/全新鲜情况
- 不可达案例
- 使用断言验证时间和剩余新鲜橘子数
10. 学习路径与延伸阅读
10.1 推荐练习题目
- 单源BFS:LeetCode 542(01矩阵)
- 多源BFS变体:LeetCode 286(墙与门)
- 加权BFS:LeetCode 505(迷宫II)
10.2 经典算法书籍参考
- 《算法导论》图算法章节
- 《算法4》中的图搜索实现
- 《编程珠玑》中的算法设计思想
在实际编码时,我发现使用层序遍历标记(如将时间戳直接写入网格)可以辅助调试。例如临时用负数表示腐烂时间,这样打印网格时能直观看到传播过程。不过要注意最终提交时需要移除这些调试代码。
另一个实用技巧是:当处理大型网格时,可以优先检查是否存在新鲜橘子被所有四个方向都隔离的情况(即上下左右都是0或边界),这种橘子永远无法被腐烂,可以提前返回-1。虽然最坏时间复杂度不变,但在实际应用中能显著提高效率。
