📌 题目链接:994. 腐烂的橘子 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:广度优先搜索(BFS)、多源 BFS、矩阵、模拟
⏱️ 目标时间复杂度:O(mn)
💾 空间复杂度:O(mn)
🧠 题目分析
本题描述了一个典型的“感染传播”过程:
- 网格中有三种状态:
0(空)、1(新鲜橘子)、2(腐烂橘子); - 每分钟,所有当前已腐烂的橘子会同时向上下左右四个方向“感染”相邻的新鲜橘子;
- 问:最少需要多少分钟才能让所有新鲜橘子都腐烂?若不可能,返回
-1。
这本质上是一个多起点的最短路径问题——多个腐烂橘子同时开始“扩散”,我们需要知道最后一个被感染的新鲜橘子是在第几分钟被感染的。
💡 关键洞察:这不是单源 BFS,而是多源 BFS(Multi-source BFS) 。我们可以把所有初始腐烂橘子看作同一层的“超级源点”,从它们同时出发进行 BFS,这样每一轮 BFS 对应一分钟的传播。
🧩 核心算法及代码讲解
✅ 算法选择:多源广度优先搜索(Multi-source BFS)
为什么用 BFS?
- 因为 BFS 天然具有“按层扩展”的特性,每一层对应一分钟;
- 所有腐烂橘子在同一时间步向外扩散,符合 BFS 的队列处理逻辑;
- 最终答案就是 BFS 的最大深度(即最后一层被感染的时间)。
📌 多源 BFS 的实现技巧:
- 初始化:将所有值为
2的位置(腐烂橘子)一次性加入队列,并标记时间为0; - 同步扩散:每次从队列中取出一个位置,向四个方向尝试感染;
- 记录时间:新感染的位置的时间 = 当前时间 + 1;
- 检查是否全部感染:若最后还有
1存在,说明无法全部腐烂,返回-1。
🧾 C++ 代码详解(含行注释):
// 初始化方向数组:右、下、左、上
int dir_x[4] = {0, 1, 0, -1};
int dir_y[4] = {1, 0, -1, 0};
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
queue<pair<int, int>> Q; // BFS 队列,存储 (行, 列)
int dis[10][10]; // 记录每个格子被感染的时间
memset(dis, -1, sizeof(dis)); // 初始化为 -1,表示未访问
int cnt = 0; // 统计新鲜橘子数量
int n = grid.size(), m = grid[0].size();
int ans = 0; // 最终答案:最大感染时间
// 第一步:遍历网格,初始化队列和计数器
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (grid[i][j] == 2) {
Q.emplace(i, j); // 腐烂橘子入队
dis[i][j] = 0; // 感染时间为 0
} else if (grid[i][j] == 1) {
cnt++; // 统计新鲜橘子
}
}
}
// 第二步:多源 BFS
while (!Q.empty()) {
auto [r, c] = Q.front(); // 取出当前腐烂橘子
Q.pop();
// 尝试四个方向
for (int i = 0; i < 4; ++i) {
int tx = r + dir_x[i];
int ty = c + dir_y[i];
// 边界检查 + 是否已访问 + 是否为空格
if (tx < 0 || tx >= n || ty < 0 || ty >= m ||
dis[tx][ty] != -1 || grid[tx][ty] == 0) {
continue;
}
// 感染成功!
dis[tx][ty] = dis[r][c] + 1; // 时间 +1
Q.emplace(tx, ty); // 新腐烂橘子入队
// 如果是新鲜橘子(值为1),才减少计数
if (grid[tx][ty] == 1) {
cnt--;
ans = dis[tx][ty]; // 更新最大时间
if (cnt == 0) break; // 提前终止优化
}
}
if (cnt == 0) break; // 外层也 break
}
// 第三步:判断是否全部感染
return cnt ? -1 : ans;
}
};
✅ 注意:虽然我们在 BFS 中修改了
dis数组,但没有修改原grid(与官方 JS 解法不同)。这是为了保持输入不变,更符合函数式编程习惯,且不影响正确性。
🧭 解题思路(分步骤)
-
预处理阶段:
- 遍历整个网格;
- 将所有腐烂橘子(
2)加入 BFS 队列,设其时间为0; - 同时统计新鲜橘子(
1)的总数cnt。
-
多源 BFS 扩散阶段:
-
从队列中逐个取出腐烂橘子;
-
向四个方向尝试感染;
-
若邻居是新鲜橘子(
1),则:- 标记其感染时间 = 当前时间 + 1;
- 入队;
cnt--;- 更新
ans = 当前时间 + 1。
-
-
结果判断阶段:
- 若
cnt > 0,说明有橘子未被感染 → 返回-1; - 否则返回
ans(即最大感染时间)。
- 若
💡 边界情况处理:
- 示例 3:
[[0,2]]→ 没有新鲜橘子,直接返回0;- 示例 2:存在被隔离的新鲜橘子 → 返回
-1。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(mn):每个格子最多入队一次,共 mn 个格子 |
| 空间复杂度 | O(mn):dis 数组 + 队列最多存 mn 个元素 |
| 是否可优化 | 可以不使用 dis 数组,直接用 grid 原地标记时间(如 JS 解法),但会破坏输入;本解法保留输入,更安全 |
| 面试考点 | 多源 BFS 的建模能力、BFS 层序遍历、边界处理、提前终止优化 |
🎯 面试高频问题:
- “如果网格很大(比如 1e5 x 1e5),还能用 BFS 吗?” → 不能,需考虑稀疏表示或并查集(但本题规模小,BFS 最优);
- “能否用 DFS?” → 不能,DFS 无法保证“最短时间”,因为感染是并行发生的;
- “如何判断是否全部感染?” → 要么计数器
cnt,要么最后扫描一遍grid。
💻 代码
✅ C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int dir_x[4] = {0, 1, 0, -1};
int dir_y[4] = {1, 0, -1, 0};
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
queue<pair<int, int>> Q;
int dis[10][10];
memset(dis, -1, sizeof(dis));
int cnt = 0;
int n = (int)grid.size(), m = (int)grid[0].size(), ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (grid[i][j] == 2) {
Q.emplace(i, j);
dis[i][j] = 0;
}
else if (grid[i][j] == 1) {
cnt += 1;
}
}
}
while (!Q.empty()){
auto [r, c] = Q.front();
Q.pop();
for (int i = 0; i < 4; ++i) {
int tx = r + dir_x[i];
int ty = c + dir_y[i];
if (tx < 0|| tx >= n || ty < 0|| ty >= m || ~dis[tx][ty] || !grid[tx][ty]) {
continue;
}
dis[tx][ty] = dis[r][c] + 1;
Q.emplace(tx, ty);
if (grid[tx][ty] == 1) {
cnt -= 1;
ans = dis[tx][ty];
if (!cnt) {
break;
}
}
}
if (!cnt) break;
}
return cnt ? -1 : ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<vector<int>> grid1 = {{2,1,1},{1,1,0},{0,1,1}};
cout << sol.orangesRotting(grid1) << "\n"; // 输出: 4
vector<vector<int>> grid2 = {{2,1,1},{0,1,1},{1,0,1}};
cout << sol.orangesRotting(grid2) << "\n"; // 输出: -1
vector<vector<int>> grid3 = {{0,2}};
cout << sol.orangesRotting(grid3) << "\n"; // 输出: 0
return 0;
}
✅ JavaScript
var orangesRotting = function(grid) {
const R = grid.length, C = grid[0].length;
const dr = [-1, 0, 1, 0];
const dc = [0, -1, 0, 1];
const queue = [];
const depth = new Map();
// 初始化:所有腐烂橘子入队
for (let r = 0; r < R; ++r) {
for (let c = 0; c < C; ++c) {
if (grid[r][c] === 2) {
const code = r * C + c;
queue.push(code);
depth.set(code, 0);
}
}
}
let ans = 0;
while (queue.length !== 0) {
const code = queue.shift();
const r = Math.floor(code / C), c = code % C;
for (let k = 0; k < 4; ++k) {
const nr = r + dr[k];
const nc = c + dc[k];
if (0 <= nr && nr < R && 0 <= nc && nc < C && grid[nr][nc] === 1) {
grid[nr][nc] = 2; // 原地修改为腐烂
const ncode = nr * C + nc;
queue.push(ncode);
depth.set(ncode, depth.get(code) + 1);
ans = depth.get(ncode);
}
}
}
// 检查是否还有新鲜橘子
const freshOrangesCount = grid.reduce((acc, row) =>
acc + row.reduce((acc, v) => acc + (v === 1 ? 1 : 0), 0), 0);
return freshOrangesCount > 0 ? -1 : ans;
};
🌟 结语
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路!关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!