腐烂的橘子

1. 题目回顾
力扣 994. 腐烂的橘子 (Rotting Oranges)
题目描述
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
- 值
0代表空单元格; - 值
1代表新鲜橘子; - 值
2代表腐烂的橘子。
每分钟,腐烂的橘子会使其上下左右 四个方向上相邻的新鲜橘子腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
示例
-
示例 1:
- 输入:
grid = [[2,1,1],[1,1,0],[0,1,1]] - 输出:
4
- 输入:
-
示例 2:
- 输入:
grid = [[2,1,1],[0,1,1],[1,0,1]] - 输出:
-1(左下角的橘子永远不会被腐烂)
- 输入:
-
示例 3:
- 输入:
grid = [[0,2]] - 输出:
0(一开始就没有新鲜橘子)
- 输入:
约束条件
m == grid.lengthn == grid[i].length- 1≤m,n≤101≤m,n≤10
grid[i][j]仅为0、1或2。
2. 核心思路:多源广度优先搜索(Multi-source BFS)
这道题是经典的多源 BFS 问题。因为每一分钟,所有腐烂的橘子会同时向四周扩散,这与 BFS 的"层级遍历"特性完美契合。
- 多源起点:普通的 BFS 只有一个起点,而这道题一开始可能有多个腐烂的橘子。我们需要将所有初始腐烂橘子的坐标同时加入队列,作为 BFS 的第 0 层。
- 层级扩散 :每次从队列中取出一批节点,将它们周围的新鲜橘子(值为
1)腐烂(改为2),并将这些新腐烂的橘子加入队列。这代表过了一分钟。 - 防重与终止 :通过修改网格值(
1变2)来充当"访问标记",避免重复入队。当队列为空时,扩散结束。 - 连通性检查 :BFS 结束后,遍历整个网格,如果还存在值为
1的新鲜橘子,说明它被0(空单元格)包围,无法被腐烂,返回-1;否则返回经过的分钟数。
3. 算法详细步骤
3.1 初始化与边界处理
- 获取网格的行数
m和列数n。 - 定义四个方向的偏移量数组
directions = {``{-1, 0}, {1, 0}, {0, -1}, {0, 1}}。 - 创建一个队列
queue,并遍历整个网格:- 统计新鲜橘子的数量
freshCount。 - 将所有腐烂橘子(值为
2)的坐标[row, col]加入队列。
- 统计新鲜橘子的数量
3.2 分解过程:BFS 层级遍历
- 如果初始时
freshCount == 0,直接返回0。 - 初始化分钟数
minutes = 0。 - 当队列不为空且
freshCount > 0时,进行循环:- 获取当前队列的大小
size(代表当前这一分钟需要处理的腐烂橘子数量)。 - 循环
size次,每次弹出一个坐标[r, c]。 - 遍历四个方向,计算相邻坐标
[nr, nc]。 - 如果相邻坐标在网格范围内,且值为
1(新鲜橘子):- 将其值改为
2(腐烂)。 freshCount--。- 将
[nr, nc]加入队列。
- 将其值改为
- 当前层级的所有节点处理完毕后,
minutes++。
- 获取当前队列的大小
3.3 解决过程:连通性检查
- BFS 结束后,检查
freshCount是否等于0。 - 如果
freshCount == 0,返回minutes。 - 如果
freshCount > 0,说明有橘子无法被腐烂,返回-1。
4. 代码实现(Java 版本 - 代码分析)
class Solution {
public int orangesRotting(int[][] grid) {
// 获取网格的行数和列数
int r = grid.length;
int c = grid[0].length;
// 使用链表作为队列,用于 BFS 广度优先搜索,存储所有腐烂橘子的坐标
LinkedList<int[]> list = new LinkedList<>();
// 记录当前网格中新鲜橘子(值为1)的数量
int count = 0;
// 遍历整个网格,初始化队列和新鲜橘子计数器
for(int i = 0; i < r; i++){
for(int j = 0; j < c; j++){
if(grid[i][j] == 2) {
// 如果是腐烂的橘子,将其坐标加入队列,作为 BFS 的起点
list.offer(new int[]{i, j});
} else if(grid[i][j] == 1) {
// 如果是新鲜的橘子,新鲜橘子计数加 1
count++;
}
}
}
// 记录经过的分钟数(即 BFS 的层数)
int round = 0;
// 当还有新鲜橘子(count != 0) 且 队列不为空时,继续循环
// 如果 count==0,说明所有橘子都已腐烂,直接退出
// 如果 list 为空但 count!=0,说明有新鲜橘子无法被传染,最后会返回 -1
while(count != 0 && !list.isEmpty()){
// 每处理完队列中当前层的所有节点,时间就过去 1 分钟
round++;
// 获取当前层(当前分钟)腐烂橘子的数量
// 这一步非常关键,用于区分"当前分钟"和"下一分钟"的橘子
int size = list.size();
// 将当前分钟内所有腐烂的橘子处理完
for(int i = 0; i < size; i++){
// 从队列头部取出一个腐烂的橘子坐标
int[] bedOrange = list.poll();
int bedR = bedOrange[0]; // 当前腐烂橘子的行坐标
int bedC = bedOrange[1]; // 当前腐烂橘子的列坐标
// 向上腐蚀:检查上方格子是否在边界内,且是否为新鲜橘子
if(bedR - 1 >= 0 && grid[bedR - 1][bedC] == 1){
count--; // 新鲜橘子数量减 1
grid[bedR - 1][bedC] = 2; // 将新鲜橘子标记为腐烂,防止重复入队
list.offer(new int[]{bedR - 1, bedC}); // 新腐烂的橘子加入队列,等待下一分钟处理
}
// 向下腐蚀:检查下方格子
if(bedR + 1 < r && grid[bedR + 1][bedC] == 1){
count--;
grid[bedR + 1][bedC] = 2;
list.offer(new int[]{bedR + 1, bedC});
}
// 向左腐蚀:检查左方格子
if(bedC - 1 >= 0 && grid[bedR][bedC - 1] == 1){
count--;
grid[bedR][bedC - 1] = 2;
list.offer(new int[]{bedR, bedC - 1});
}
// 向右腐蚀:检查右方格子
if(bedC + 1 < c && grid[bedR][bedC + 1] == 1){
count--;
grid[bedR][bedC + 1] = 2;
list.offer(new int[]{bedR, bedC + 1});
}
}
}
// 循环结束后,如果 count == 0,说明所有新鲜橘子都被腐蚀了,返回经过的分钟数
// 如果 count != 0,说明存在被孤立的新鲜橘子,无法被腐蚀,返回 -1
return count == 0 ? round : -1;
}
}
代码关键点分析
- 多源入队 :一开始将所有
2都放入队列,这是解决"同时腐烂"的关键。如果只用一个起点进行 BFS,会导致时间计算错误。 - 层级控制 :
int size = queue.size();这一行极其重要。它确保了for循环只处理当前分钟已经腐烂的橘子,而它们在循环中新加入队列的橘子会在下一分钟(外层while的下一次循环)被处理。 - 原地修改充当 Visited 数组 :直接将
grid[nr][nc] = 2,既改变了状态,又防止了该节点被其他方向的腐烂橘子重复入队,节省了 O(m×n) 的空间。
5. 复杂度分析
- 时间复杂度: O(m×n) 。每个单元格最多被访问常数次(初始遍历一次,BFS 中最多入队出队一次)。
- 空间复杂度: O(m×n)。在最坏情况下(例如整个网格全是腐烂的橘子),队列中需要存储所有的坐标。