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

参考灵神

相关推荐
能工智人小辰5 分钟前
二刷 黑马点评 秒杀优化
java·开发语言
杨小扩5 分钟前
夯实基础:配置Java开发环境JDK与构建工具Maven
java·开发语言·maven
Gyoku Mint11 分钟前
深度学习×第10卷:她用一块小滤镜,在图像中找到你
人工智能·python·深度学习·神经网络·opencv·算法·cnn
随风ada16 分钟前
Windows、macOS、liunx下使用qemu搭建riscv64/linux
linux·windows·ubuntu·macos·golang·qemu·risc-v
智者知已应修善业22 分钟前
2021-07-21 VB窗体求范围质数(Excel复制工作簿)
经验分享·笔记·算法
开开心心_Every34 分钟前
免费PDF文件格式转换工具
java·智能手机·pdf·word·batch·java-zookeeper
Kiri霧42 分钟前
Kotlin集合分组
android·java·前端·kotlin
C++chaofan1 小时前
45. 跳跃游戏 II
java·开发语言·数据结构·算法·leetcode·游戏·职场和发展
Ciderw1 小时前
leetcode15.三数之和题解:逻辑清晰带你分析
开发语言·c++·笔记·学习·leetcode
艾特小小1 小时前
spring-cloud微服务部署-feign服务间调用
java·spring boot·spring cloud·微服务·架构