1. 问题背景与场景还原
第一次在LeetCode上看到"腐烂的橘子"这道题时,我正坐在一家咖啡馆里。题目描述让我想起上周清理冰箱时发现的场景——几个发霉的橘子周围,原本新鲜的水果也开始出现褐色斑点。这道题完美模拟了这种腐败扩散的过程,只不过用二维网格代替了冰箱隔层。
题目给定一个m×n的网格,每个单元格可以是以下三种状态之一:
- 0 代表空单元格
- 1 代表新鲜橘子
- 2 代表腐烂的橘子
每分钟,任何与腐烂橘子相邻(4个方向)的新鲜橘子都会腐烂。我们需要计算直到没有新鲜橘子被腐烂为止需要的时间,或者返回-1表示不可能使所有橘子腐烂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法解析:多源广度优先搜索
2.1 为什么选择BFS而非DFS
当我第一次尝试解决这个问题时,直觉告诉我应该用广度优先搜索(BFS)。原因在于腐败的扩散具有"等时性"——所有在同一时间被污染的橘子,其"污染力"会在下一分钟同时生效。这与BFS按层遍历的特性完美契合。
相比之下,深度优先搜索(DFS)会沿着一条路径深入,无法准确模拟同时发生的多源污染过程。举个生活例子:如果用DFS,就像让一个检查员逐个检查每个橘子,而BFS则是派出一队检查员同时检查所有可能被污染的橘子。
2.2 多源BFS的实现要点
多源BFS与常规BFS的主要区别在于初始队列的构建。我们需要:
- 遍历整个网格,记录所有腐烂橘子的位置和初始时间(0分钟)
- 同时统计新鲜橘子的总数
python复制from collections import deque
def orangesRotting(grid):
m, n = len(grid), len(grid[0])
queue = deque()
fresh = 0
time = 0
# 初始化队列和新鲜橘子计数
for i in range(m):
for j in range(n):
if grid[i][j] == 2:
queue.append((i, j, 0))
elif grid[i][j] == 1:
fresh += 1
2.3 方向数组的巧妙运用
在处理二维网格的相邻单元格时,使用方向数组可以使代码更简洁:
python复制directions = [(-1,0), (1,0), (0,-1), (0,1)] # 上下左右
这比写四个独立的if条件更优雅,也更容易扩展(比如如果需要考虑对角线方向)。
3. 完整算法实现与逐行解析
3.1 主循环逻辑
python复制while queue and fresh > 0:
i, j, time = queue.popleft()
for di, dj in directions:
ni, nj = i + di, j + dj
if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] == 1:
grid[ni][nj] = 2
fresh -= 1
queue.append((ni, nj, time + 1))
这段代码有几个关键点需要注意:
- 循环条件
fresh > 0:当没有新鲜橘子时提前终止,优化性能 - 边界检查
0 <= ni < m and 0 <= nj < n:避免数组越界 - 时间记录
time + 1:每个新腐烂的橘子的时间是父节点时间+1
3.2 结果判断
循环结束后,我们需要检查是否还有新鲜橘子剩余:
python复制return time if fresh == 0 else -1
这里有个易错点:最后一次有效的腐败扩散后,time变量记录的是导致这次扩散的时间,而不是整个过程的总时间。但因为题目要求的是"直到没有新鲜橘子被腐烂为止需要的时间",这个定义与我们的实现一致。
4. 时空复杂度分析与优化思路
4.1 时间复杂度
算法需要:
- 初始化时遍历整个网格:O(m×n)
- BFS过程每个节点最多被访问一次:O(m×n)
因此总时间复杂度为O(m×n),这是最优的,因为我们必须检查每个单元格至少一次。
4.2 空间复杂度
主要空间消耗来自:
- 队列:最坏情况下所有橘子都是腐烂的,O(m×n)
- 方向数组:O(1)
因此空间复杂度也是O(m×n)。在实际面试中,如果网格非常大,可以讨论是否允许修改输入网格来减少空间使用(比如用特殊值标记已处理的节点)。
5. 边界条件与测试案例设计
5.1 必须考虑的边界情况
- 网格为空:根据题意返回0或-1(需明确)
- 没有新鲜橘子:立即返回0
- 有无法被腐烂的新鲜橘子:返回-1
- 多个腐烂橘子同时扩散
- 网格中只有一行或一列
5.2 实用测试案例
python复制测试案例1:所有橘子都已腐烂
输入:[[2,2],[2,2]]
预期输出:0
测试案例2:有无法腐烂的橘子
输入:[[2,1,1],[0,1,1],[1,0,1]]
预期输出:-1
测试案例3:常规情况
输入:[[2,1,1],[1,1,0],[0,1,1]]
预期输出:4
6. 实际编码中的陷阱与技巧
6.1 常见错误
- 忘记统计初始新鲜橘子数量,导致无法判断是否所有橘子都能被腐烂
- 在BFS中错误地更新时间(如在循环开始而非结束时更新)
- 方向数组定义不全,漏掉某些相邻方向
- 边界检查不完整,导致数组越界
6.2 调试技巧
在解决这个问题时,我发现画出网格状态随时间的演变非常有帮助。例如:
code复制初始状态:
2 1 1
1 1 0
0 1 1
第1分钟后:
2 2 1
2 1 0
0 1 1
第2分钟后:
2 2 2
2 2 0
0 1 1
...
这种可视化方法可以帮助验证算法的正确性,特别是在处理复杂边界条件时。
7. 算法变种与扩展思考
7.1 如果橘子可以斜对角腐烂
如果题目改为8个方向都可以传播腐败,我们只需要修改方向数组:
python复制directions = [(-1,-1), (-1,0), (-1,1),
(0,-1), (0,1),
(1,-1), (1,0), (1,1)]
这个改变会影响腐败传播的速度,但算法框架保持不变。
7.2 不同腐烂速度的橘子
假设有些橘子腐烂得快(每分钟可以传播更远),这就会变成一个带权图的最短路径问题,可能需要使用Dijkstra算法而不是BFS。
7.3 实际应用场景
这个问题看似简单,但其核心的多源扩散模型在现实中有着广泛应用:
- 计算机网络中的病毒传播模拟
- 森林火灾蔓延预测
- 城市交通拥堵扩散分析
- 流行病学中的疾病传播模型
在准备技术面试时,理解这些算法背后的现实意义不仅能帮助记忆,还能在系统设计面试中提供有价值的思路。
