力扣实训 _ [994].腐烂的橘子/图论

腐烂的橘子

1. 题目回顾

力扣 994. 腐烂的橘子 (Rotting Oranges)

题目描述

在给定的 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 (左下角的橘子永远不会被腐烂)
  • 示例 3:

    • 输入:grid = [[0,2]]
    • 输出:0 (一开始就没有新鲜橘子)

约束条件

  • m == grid.length
  • n == grid[i].length
  • 1≤m,n≤101≤m,n≤10
  • grid[i][j] 仅为 0、1 或 2。

2. 核心思路:多源广度优先搜索(Multi-source BFS)

这道题是经典的多源 BFS 问题。因为每一分钟,所有腐烂的橘子会同时向四周扩散,这与 BFS 的"层级遍历"特性完美契合。

  • 多源起点:普通的 BFS 只有一个起点,而这道题一开始可能有多个腐烂的橘子。我们需要将所有初始腐烂橘子的坐标同时加入队列,作为 BFS 的第 0 层。
  • 层级扩散 :每次从队列中取出一批节点,将它们周围的新鲜橘子(值为 1)腐烂(改为 2),并将这些新腐烂的橘子加入队列。这代表过了一分钟。
  • 防重与终止 :通过修改网格值(1 变 2)来充当"访问标记",避免重复入队。当队列为空时,扩散结束。
  • 连通性检查 :BFS 结束后,遍历整个网格,如果还存在值为 1 的新鲜橘子,说明它被 0(空单元格)包围,无法被腐烂,返回 -1;否则返回经过的分钟数。

3. 算法详细步骤

3.1 初始化与边界处理

  • 获取网格的行数 m 和列数 n。
  • 定义四个方向的偏移量数组 directions = {``{-1, 0}, {1, 0}, {0, -1}, {0, 1}}。
  • 创建一个队列 queue,并遍历整个网格:
    • 统计新鲜橘子的数量 freshCount。
    • 将所有腐烂橘子(值为 2)的坐标 [row, col] 加入队列。

3.2 分解过程:BFS 层级遍历

  • 如果初始时 freshCount == 0,直接返回 0。
  • 初始化分钟数 minutes = 0。
  • 当队列不为空且 freshCount > 0 时,进行循环:
    • 获取当前队列的大小 size(代表当前这一分钟需要处理的腐烂橘子数量)。
    • 循环 size 次,每次弹出一个坐标 [r, c]。
    • 遍历四个方向,计算相邻坐标 [nr, nc]。
    • 如果相邻坐标在网格范围内,且值为 1(新鲜橘子):
      • 将其值改为 2(腐烂)。
      • freshCount--。
      • 将 [nr, nc] 加入队列。
    • 当前层级的所有节点处理完毕后,minutes++。

3.3 解决过程:连通性检查

  • BFS 结束后,检查 freshCount 是否等于 0。
  • 如果 freshCount == 0,返回 minutes。
  • 如果 freshCount > 0,说明有橘子无法被腐烂,返回 -1。

4. 代码实现(Java 版本 - 代码分析)

复制代码
class Solution {
    public int orangesRotting(int[][] grid) {
        // 获取网格的行数和列数
        int r = grid.length;
        int c = grid[0].length;
        
        // 使用链表作为队列,用于 BFS 广度优先搜索,存储所有腐烂橘子的坐标
        LinkedList<int[]> list = new LinkedList<>();
        
        // 记录当前网格中新鲜橘子(值为1)的数量
        int count = 0;
        
        // 遍历整个网格,初始化队列和新鲜橘子计数器
        for(int i = 0; i < r; i++){
            for(int j = 0; j < c; j++){
                if(grid[i][j] == 2) {
                    // 如果是腐烂的橘子,将其坐标加入队列,作为 BFS 的起点
                    list.offer(new int[]{i, j});
                } else if(grid[i][j] == 1) {
                    // 如果是新鲜的橘子,新鲜橘子计数加 1
                    count++;
                }
            }
        }

        // 记录经过的分钟数(即 BFS 的层数)
        int round = 0;
        
        // 当还有新鲜橘子(count != 0) 且 队列不为空时,继续循环
        // 如果 count==0,说明所有橘子都已腐烂,直接退出
        // 如果 list 为空但 count!=0,说明有新鲜橘子无法被传染,最后会返回 -1
        while(count != 0 && !list.isEmpty()){
            // 每处理完队列中当前层的所有节点,时间就过去 1 分钟
            round++;
            
            // 获取当前层(当前分钟)腐烂橘子的数量
            // 这一步非常关键,用于区分"当前分钟"和"下一分钟"的橘子
            int size = list.size();
            
            // 将当前分钟内所有腐烂的橘子处理完
            for(int i = 0; i < size; i++){
                // 从队列头部取出一个腐烂的橘子坐标
                int[] bedOrange = list.poll();
                int bedR = bedOrange[0]; // 当前腐烂橘子的行坐标
                int bedC = bedOrange[1]; // 当前腐烂橘子的列坐标
                
                // 向上腐蚀:检查上方格子是否在边界内,且是否为新鲜橘子
                if(bedR - 1 >= 0 && grid[bedR - 1][bedC] == 1){
                    count--; // 新鲜橘子数量减 1
                    grid[bedR - 1][bedC] = 2; // 将新鲜橘子标记为腐烂,防止重复入队
                    list.offer(new int[]{bedR - 1, bedC}); // 新腐烂的橘子加入队列,等待下一分钟处理
                }
                
                // 向下腐蚀:检查下方格子
                if(bedR + 1 < r && grid[bedR + 1][bedC] == 1){
                    count--;
                    grid[bedR + 1][bedC] = 2;
                    list.offer(new int[]{bedR + 1, bedC});
                }
                
                // 向左腐蚀:检查左方格子
                if(bedC - 1 >= 0 && grid[bedR][bedC - 1] == 1){
                    count--;
                    grid[bedR][bedC - 1] = 2;
                    list.offer(new int[]{bedR, bedC - 1});
                }
                
                // 向右腐蚀:检查右方格子
                if(bedC + 1 < c && grid[bedR][bedC + 1] == 1){
                    count--;
                    grid[bedR][bedC + 1] = 2;
                    list.offer(new int[]{bedR, bedC + 1});
                }
            }
        }
        
        // 循环结束后,如果 count == 0,说明所有新鲜橘子都被腐蚀了,返回经过的分钟数
        // 如果 count != 0,说明存在被孤立的新鲜橘子,无法被腐蚀,返回 -1
        return count == 0 ? round : -1;
    }
}

代码关键点分析

  • 多源入队 :一开始将所有 2 都放入队列,这是解决"同时腐烂"的关键。如果只用一个起点进行 BFS,会导致时间计算错误。
  • 层级控制 :int size = queue.size(); 这一行极其重要。它确保了 for 循环只处理当前分钟已经腐烂的橘子,而它们在循环中新加入队列的橘子会在下一分钟(外层 while 的下一次循环)被处理。
  • 原地修改充当 Visited 数组 :直接将 grid[nr][nc] = 2,既改变了状态,又防止了该节点被其他方向的腐烂橘子重复入队,节省了 O(m×n) 的空间。

5. 复杂度分析

  • 时间复杂度: O(m×n) 。每个单元格最多被访问常数次(初始遍历一次,BFS 中最多入队出队一次)。
  • 空间复杂度: O(m×n)。在最坏情况下(例如整个网格全是腐烂的橘子),队列中需要存储所有的坐标。
相关推荐
yi0112 小时前
LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置”
数据结构·人工智能·笔记·python·算法·leetcode·哈希表
灯澜忆梦5 小时前
【面向对象编程C++】| 基础语法
java·c++·算法
小蒋观天下5 小时前
专项方案:大场景港口AI安防、多干扰环境下的算法调优与落地实操
人工智能·深度学习·算法·安全·机器学习·计算机视觉·ai大模型
Evand J5 小时前
【电机滤波例程5】渐消因子自适应扩展卡尔曼滤波(EKF),MATLAB。PMSM负载突变下的状态估计。附下载链接
算法·matlab
H.莓飛5 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法
好想像大佬一样能够ak所有6 小时前
Leetcode35.搜索插入位置
数据结构·算法·leetcode
aqiu1111116 小时前
【C++算法打怪专栏】LeetCode 165. 比较版本号(双指针与字符串流法)
c++·算法·leetcode
黄金龙PLUS6 小时前
如何基于置换算法来设计对称加密算法
算法·网络安全·密码学·哈希算法·同态加密
不会就选b6 小时前
算法日常・每日刷题--<动态规划>14
算法
赛博守夜人6 小时前
跨境业务与出海合规之九:海外买量投放与营销虚假流量(Ad Fraud):跨国机房群控与归因欺诈识别算法
算法·安全