【LeetCode Hot100 刷题日记 (52/100)】994. 腐烂的橘子 —— 多源 BFS 模拟🧠

3 阅读6分钟

📌 题目链接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 的实现技巧:

  1. 初始化:将所有值为 2 的位置(腐烂橘子)一次性加入队列,并标记时间为 0
  2. 同步扩散:每次从队列中取出一个位置,向四个方向尝试感染;
  3. 记录时间:新感染的位置的时间 = 当前时间 + 1;
  4. 检查是否全部感染:若最后还有 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 解法不同)。这是为了保持输入不变,更符合函数式编程习惯,且不影响正确性。


🧭 解题思路(分步骤)

  1. 预处理阶段

    • 遍历整个网格;
    • 将所有腐烂橘子(2)加入 BFS 队列,设其时间为 0
    • 同时统计新鲜橘子(1)的总数 cnt
  2. 多源 BFS 扩散阶段

    • 从队列中逐个取出腐烂橘子;

    • 向四个方向尝试感染;

    • 若邻居是新鲜橘子(1),则:

      • 标记其感染时间 = 当前时间 + 1;
      • 入队;
      • cnt--
      • 更新 ans = 当前时间 + 1
  3. 结果判断阶段

    • 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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!