【LeetCode 热题 100】994. 腐烂的橘子——BFS

Problem: 994. 腐烂的橘子
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

  • 值 0 代表空单元格;
  • 值 1 代表新鲜橘子;
  • 值 2 代表腐烂的橘子。

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。

整体思路

这段代码旨在解决一个经典的图论问题:腐烂的橘子 (Rotting Oranges)。问题是在一个二维网格中,计算所有新鲜橘子(值为1)全部变为腐烂橘子(值为2)所需的最短时间。如果有些橘子永远不会腐烂,则返回-1。

该算法采用的核心方法是 广度优先搜索(BFS),并且是 多源BFS (Multi-source BFS) 的一个典型应用。BFS之所以是解决此问题的完美模型,是因为橘子腐烂的过程是逐层、同时向外扩散的,这与BFS逐层探索图节点的行为完全一致。

算法的整体思路可以分解为以下几个步骤:

  1. 初始化与状态扫描

    • 算法首先遍历整个网格 grid
    • 目的
      a. 统计所有新鲜橘子的数量(fresh)。这个计数器是判断最终是否所有橘子都腐烂的关键。
      b. 找到所有初始的腐烂橘子,并将它们的坐标作为BFS的初始源点,加入到一个队列 q 中。
  2. 逐层BFS模拟腐烂过程

    • 算法的主体是一个 while 循环,这个循环模拟了时间一分钟一分钟地流逝。
    • 时间模拟:每一次 while 循环的完整迭代代表“一分钟”。因此,在循环开始时,将时间计数器 ans 加一。
    • 分层处理:为了确保按分钟(层)处理,代码使用了一个巧妙的技巧。在每一分钟开始时,它将当前队列 q 的内容暂存到 temp 列表中,然后将 q 重置为一个新的空列表。接着,它只处理 temp 列表中的橘子(即上一分钟刚刚腐烂或已腐烂的橘子)。这些橘子产生的新腐烂橘子将被加入到新的 q 中,为下一分钟的处理做准备。
  3. 扩散与状态更新

    • 在每一分钟的模拟中,遍历 temp 列表里的每一个腐烂橘子。
    • 对于每个腐烂橘子,检查其上、下、左、右四个相邻的格子。
    • 如果一个相邻的格子在网格范围内,并且包含一个新鲜橘子(值为1),则这个新鲜橘子会被腐烂。
    • 状态更新
      a. 将新鲜橘子计数器 fresh 减一。
      b. 将网格上该位置的值从 1 更新为 2,以标记它已腐烂,并防止后续被重复处理。
      c. 将这个新腐烂的橘子的坐标加入到新的队列 q 中,它将成为下一分钟的腐烂源。
  4. 终止条件与结果判断

    • while 循环在以下两种情况之一发生时终止:
      a. fresh == 0:所有新鲜橘子都已腐烂,任务完成。
      b. q.isEmpty():队列为空,意味着没有更多的腐烂橘子可以继续扩散了,但此时可能仍有 fresh > 0
    • 循环结束后,检查 fresh 的值。如果 fresh > 0,说明有新鲜橘子与任何腐烂源都不连通,永远无法腐烂,按要求返回 -1。否则,返回累计的时间 ans

完整代码

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)

  1. 初始扫描:第一个嵌套的 for 循环遍历了整个 M x N 的网格一次,时间复杂度为 O(M * N)
  2. BFS过程
    • while 循环中,每个格子最多被访问一次。当一个新鲜橘子 (i, j) 变为腐烂时,它会被加入队列 q 一次,然后从队列中取出来处理一次。
    • 在处理每个格子时,我们会检查其四个邻居,这是常数时间的操作。
    • 因此,整个BFS过程访问了所有格子最多一次,总的时间复杂度与网格的大小成正比,为 O(M * N)

综合分析
算法的总时间复杂度是 O(M * N) (扫描) + O(M * N) (BFS) = O(M * N),其中 M 和 N 分别是网格的行数和列数。

空间复杂度:O(M * N)

  1. 主要存储开销:空间复杂度主要由BFS队列 q 决定。
  2. 空间大小:在最坏的情况下,队列中可能需要存储大量的橘子坐标。例如,如果网格中除了一个角落的橘子是新鲜的,其他所有橘子都是腐烂的,那么初始队列 q 的大小接近 M * N。或者,如果第一行全是腐烂橘子,其他行全是新鲜橘-子,那么在第一分钟后,第二行的所有新鲜橘子都会被加入队列。
  3. 最坏情况:队列 q 的最大大小可以达到 O(M * N)

综合分析
算法所需的额外空间取决于BFS队列的最大尺寸,因此其空间复杂度为 O(M * N)

参考灵神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xumistore

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值