腐烂的橘子

在给定的 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
解释:左下角的橘子永远不会腐烂,因为腐烂只会发生在 4 个方向上。

示例 3:

输入:grid = [[0,2]]
输出:0
解释:0 分钟时已经没有新鲜橘子了,所以答案就是 0。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 10
  • grid[i][j] 仅为 012

多源 BFS:

class Solution {
public:
    int orangesRotting(vector<vector<int>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        int freshCount = 0;
        queue<pair<int, int>> rotten;

        for (int row = 0; row < m; ++row)
        {
            for (int col = 0; col < n; ++col)
            {
                if (grid[row][col] == 1)
                {
                    ++freshCount;
                }
                else if (grid[row][col] == 2)
                {
                    rotten.push({row, col});
                }
            }
        }

        if (freshCount == 0)
        {
            return 0;
        }

        int minutes = 0;
        int directions[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

        while (!rotten.empty())
        {
            int size = rotten.size();
            bool hasNewRotten = false;

            for (int i = 0; i < size; ++i)
            {
                auto [row, col] = rotten.front();
                rotten.pop();

                for (const auto& direction : directions)
                {
                    int nextRow = row + direction[0];
                    int nextCol = col + direction[1];

                    if (nextRow < 0 || nextRow >= m || nextCol < 0 || nextCol >= n)
                    {
                        continue;
                    }
                    if (grid[nextRow][nextCol] != 1)
                    {
                        continue;
                    }

                    grid[nextRow][nextCol] = 2;
                    --freshCount;
                    hasNewRotten = true;
                    rotten.push({nextRow, nextCol});
                }
            }

            if (hasNewRotten)
            {
                ++minutes;
            }
        }

        return freshCount == 0 ? minutes : -1;
    }
};

核心思想

将腐烂过程看成按层扩散

腐烂会在每一分钟同时发生:

  • 0 分钟:网格中原本腐烂的橘子已经腐烂。
  • 1 分钟:距离初始腐烂橘子一步的新鲜橘子腐烂。
  • 2 分钟:距离初始腐烂橘子两步的新鲜橘子腐烂。

这正好符合 BFS 的按层遍历特征。

因此,应该把所有初始腐烂橘子同时加入队列,作为 BFS 的多个起点,而不是从某一个腐烂橘子单独开始搜索。

为什么是多源 BFS

如果网格中有多个腐烂橘子,它们会同时向外扩散。

某个新鲜橘子腐烂所需的时间,等于它到最近的初始腐烂橘子的最短四方向距离。

BFS 天然按照距离从小到大访问位置,所以多源 BFS 第一次腐烂某个橘子的时刻,就是它能够腐烂的最早时刻。

队列中存什么

队列中保存当前已经腐烂、并且还需要继续向外扩散的橘子坐标。

每一轮循环开始时,队列中已有的元素都属于同一分钟会向外扩散的腐烂橘子。

因此先记录当前队列大小 size,只处理这 size 个元素,就能保证一轮循环对应一分钟的传播范围。

计时方式

代码使用变量 minutes 记录已经发生了多少分钟的有效腐烂传播。

每一层 BFS 处理完后,只有当这一层真的让至少一个新鲜橘子腐烂时,才让 minutes 加一:

if (hasNewRotten)
{
    ++minutes;
}

这样可以避免最后一层腐烂橘子已经无法继续感染新鲜橘子时,额外多计算一分钟。

如果一开始就没有新鲜橘子,直接返回 0,因为不需要等待任何腐烂过程。

为什么需要统计新鲜橘子数量

变量 freshCount 表示当前还没有腐烂的新鲜橘子数量。

每当一个新鲜橘子被腐烂,就将它改成 2,并令 freshCount 减一。

BFS 结束后:

  • 如果 freshCount == 0,说明所有新鲜橘子都已经腐烂,返回经过的分钟数。
  • 如果 freshCount > 0,说明仍有新鲜橘子无法被任何腐烂橘子到达,返回 -1

正确性证明

结论 1:BFS 可以模拟每分钟同时扩散

算法一开始将所有初始腐烂橘子加入队列,它们都属于第 0 分钟的腐烂源。

每轮 BFS 只处理当前队列中已有的橘子,并把新腐烂的橘子加入队列尾部,留到下一轮再继续扩散。

因此第 t 轮中新腐烂的橘子,恰好是在第 t 分钟被腐烂的橘子,算法与题目的同步扩散过程一致。

结论 2:每个橘子第一次腐烂的时间最小

BFS 按照从初始腐烂橘子的四方向距离由近到远扩展。

当某个新鲜橘子第一次被访问时,说明已经找到一条从某个初始腐烂橘子到它的最短路径。若存在更早腐烂它的方式,那么它会在更早的 BFS 层中被访问,与第一次访问发生在当前层矛盾。

因此算法记录的腐烂时间是该橘子可能腐烂的最早时间。

结论 3:返回 -1 当且仅当存在无法腐烂的新鲜橘子

BFS 会沿四方向访问所有能够从初始腐烂橘子到达的新鲜橘子,并将它们腐烂。

如果 BFS 结束后仍有 freshCount > 0,这些新鲜橘子无法通过四方向路径连接到任何初始腐烂橘子,所以不可能腐烂。

反之,如果所有新鲜橘子都能被腐烂,BFS 一定会访问并腐烂它们,最终 freshCount == 0

综上,算法能够正确返回使所有橘子腐烂所需的最小分钟数;若无法全部腐烂,则正确返回 -1

示例分析

grid = [[2,1,1],[1,1,0],[0,1,1]] 为例:

第 0 分钟:初始腐烂橘子在 (0,0)
第 1 分钟:(0,1)、(1,0) 腐烂
第 2 分钟:(0,2)、(1,1) 腐烂
第 3 分钟:(2,1) 腐烂
第 4 分钟:(2,2) 腐烂

所有新鲜橘子都腐烂后,共经过 4 分钟。

复杂度分析

  • 时间复杂度:O(mn)。每个格子最多入队一次,并且每次只检查四个方向。
  • 空间复杂度:O(mn)。最坏情况下,队列中可能存放所有格子的坐标。

边界情况

  • 初始时没有新鲜橘子:直接返回 0
  • 没有腐烂橘子但存在新鲜橘子:BFS 无法启动,最终返回 -1
  • 新鲜橘子被空单元格隔开:无法通过四方向传播到达,最终返回 -1
  • 网格只有一行或一列:边界判断保证只访问合法的相邻位置。