Problem: 994. 腐烂的橘子
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
- 值 0 代表空单元格;
- 值 1 代表新鲜橘子;
- 值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
文章目录
整体思路
这段代码旨在解决一个经典的图论问题:腐烂的橘子 (Rotting Oranges)。问题是在一个二维网格中,计算所有新鲜橘子(值为1)全部变为腐烂橘子(值为2)所需的最短时间。如果有些橘子永远不会腐烂,则返回-1。
该算法采用的核心方法是 广度优先搜索(BFS) ,并且是 多源BFS (Multi-source BFS) 的一个典型应用。BFS之所以是解决此问题的完美模型,是因为橘子腐烂的过程是逐层、同时向外扩散的,这与BFS逐层探索图节点的行为完全一致。
算法的整体思路可以分解为以下几个步骤:
-
初始化与状态扫描:
- 算法首先遍历整个网格
grid
。 - 目的 :
a. 统计所有新鲜橘子 的数量(fresh
)。这个计数器是判断最终是否所有橘子都腐烂的关键。
b. 找到所有初始的腐烂橘子 ,并将它们的坐标作为BFS的初始源点,加入到一个队列q
中。
- 算法首先遍历整个网格
-
逐层BFS模拟腐烂过程:
- 算法的主体是一个
while
循环,这个循环模拟了时间一分钟一分钟地流逝。 - 时间模拟 :每一次
while
循环的完整迭代代表"一分钟"。因此,在循环开始时,将时间计数器ans
加一。 - 分层处理 :为了确保按分钟(层)处理,代码使用了一个巧妙的技巧。在每一分钟开始时,它将当前队列
q
的内容暂存到temp
列表中,然后将q
重置为一个新的空列表。接着,它只处理temp
列表中的橘子(即上一分钟刚刚腐烂或已腐烂的橘子)。这些橘子产生的新腐烂橘子将被加入到新的q
中,为下一分钟的处理做准备。
- 算法的主体是一个
-
扩散与状态更新:
- 在每一分钟的模拟中,遍历
temp
列表里的每一个腐烂橘子。 - 对于每个腐烂橘子,检查其上、下、左、右四个相邻的格子。
- 如果一个相邻的格子在网格范围内,并且包含一个新鲜橘子(值为1),则这个新鲜橘子会被腐烂。
- 状态更新 :
a. 将新鲜橘子计数器fresh
减一。
b. 将网格上该位置的值从1
更新为2
,以标记它已腐烂,并防止后续被重复处理。
c. 将这个新腐烂的橘子的坐标加入到新的 队列q
中,它将成为下一分钟的腐烂源。
- 在每一分钟的模拟中,遍历
-
终止条件与结果判断:
while
循环在以下两种情况之一发生时终止:
a.fresh == 0
:所有新鲜橘子都已腐烂,任务完成。
b.q.isEmpty()
:队列为空,意味着没有更多的腐烂橘子可以继续扩散了,但此时可能仍有fresh > 0
。- 循环结束后,检查
fresh
的值。如果fresh > 0
,说明有新鲜橘子与任何腐烂源都不连通,永远无法腐烂,按要求返回 -1。否则,返回累计的时间ans
。
完整代码
java
class Solution {
// 定义四个方向的常量数组,便于在 grid 中进行上、下、左、右的移动
private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int orangesRotting(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
// ans: 最终的时间(分钟数),即BFS的层数
int ans = 0;
// fresh: 记录新鲜橘子的总数
int fresh = 0;
// q: BFS 队列,存储所有腐烂橘子的坐标 [i, j]
List<int[]> q = new ArrayList<>();
// 步骤 1: 初始化扫描,统计新鲜橘子数量并找到所有初始的腐烂橘子源
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) {
q.add(new int[]{i, j});
}
}
}
// 步骤 2: 进行多源BFS
// 循环条件:当还有新鲜橘子存在,并且还有腐烂源可以扩散时
while (fresh > 0 && !q.isEmpty()) {
// 新一轮的扩散开始,时间加一分钟
ans++;
// 分层BFS的核心:处理当前层的所有节点,并将下一层的节点存入新队列
List<int[]> temp = q;
q = new ArrayList<>();
// 遍历当前层的所有腐烂橘子
for (int[] pos : temp) {
// 探索其四个方向的邻居
for (int[] d : DIRECTIONS) {
int i = pos[0] + d[0];
int j = pos[1] + d[1];
// 检查邻居是否在网格内,并且是一个新鲜橘子
if (i >= 0 && i < m && j >= 0 && j < n && grid[i][j] == 1) {
// 该新鲜橘子被腐烂
fresh--;
// 更新网格状态,标记为腐烂,防止重复访问
grid[i][j] = 2;
// 将新腐烂的橘子加入下一轮的处理队列
q.add(new int[]{i, j});
}
}
}
}
// 步骤 4: 返回结果
// 如果循环结束后 fresh 仍然大于 0,说明有橘子无法被腐烂
// 否则,返回总耗时 ans
return fresh > 0 ? -1 : ans;
}
}
时空复杂度
时间复杂度:O(M * N)
- 初始扫描 :第一个嵌套的
for
循环遍历了整个M x N
的网格一次,时间复杂度为 O(M * N)。 - BFS过程 :
- 在
while
循环中,每个格子最多被访问一次。当一个新鲜橘子(i, j)
变为腐烂时,它会被加入队列q
一次,然后从队列中取出来处理一次。 - 在处理每个格子时,我们会检查其四个邻居,这是常数时间的操作。
- 因此,整个BFS过程访问了所有格子最多一次,总的时间复杂度与网格的大小成正比,为 O(M * N)。
- 在
综合分析 :
算法的总时间复杂度是 O(M * N) (扫描) + O(M * N) (BFS) = O(M * N),其中 M 和 N 分别是网格的行数和列数。
空间复杂度:O(M * N)
- 主要存储开销 :空间复杂度主要由BFS队列
q
决定。 - 空间大小 :在最坏的情况下,队列中可能需要存储大量的橘子坐标。例如,如果网格中除了一个角落的橘子是新鲜的,其他所有橘子都是腐烂的,那么初始队列
q
的大小接近M * N
。或者,如果第一行全是腐烂橘子,其他行全是新鲜橘-子,那么在第一分钟后,第二行的所有新鲜橘子都会被加入队列。 - 最坏情况 :队列
q
的最大大小可以达到O(M * N)
。
综合分析 :
算法所需的额外空间取决于BFS队列的最大尺寸,因此其空间复杂度为 O(M * N)。
参考灵神