【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。

完整代码

java 复制代码
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)。

参考灵神

相关推荐
谢亮_vipxieliang7 小时前
泛型擦除、通配符与 PECS 一次讲透
java·面试
八解毒剂7 小时前
【计组】中央处理器CPU
java·开发语言·计算机组成原理
乐迪信息8 小时前
港口船舶逆行怎么管?AI防爆摄像机搭载智能检测算法
大数据·人工智能·深度学习·算法·计算机视觉
打工仔折腾 AI8 小时前
工业场景下时序库与实时计算一体化架构选型实践对比
java·开发语言·后端·python·性能优化·架构·ai agent 实战
ZealSinger8 小时前
Java虚拟线程上线后瓶颈转移到哪看三层
java·开发语言·firefox
谷哥的小弟8 小时前
CentOS Stream浴火重生
linux·运维·centos
2601_961133288 小时前
盘立方软件指标文华期货公式下载
java
蜗牛互联网8 小时前
长任务多Agent共享文件系统的Manifest交接模式
java·人工智能·后端
Sirens.8 小时前
Java并发锁详解:六类锁策略与 synchronized 底层原理
java·前端·算法
程序员-Benothing9 小时前
Shell输入输出详解:read、echo、printf、颜色输出
linux·运维·服务器