力扣实训 _ [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] 仅为 012

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

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

  • 多源起点:普通的 BFS 只有一个起点,而这道题一开始可能有多个腐烂的橘子。我们需要将所有初始腐烂橘子的坐标同时加入队列,作为 BFS 的第 0 层。
  • 层级扩散 :每次从队列中取出一批节点,将它们周围的新鲜橘子(值为 1)腐烂(改为 2),并将这些新腐烂的橘子加入队列。这代表过了一分钟。
  • 防重与终止 :通过修改网格值(12)来充当"访问标记",避免重复入队。当队列为空时,扩散结束。
  • 连通性检查 :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)。在最坏情况下(例如整个网格全是腐烂的橘子),队列中需要存储所有的坐标。
相关推荐
轻微的风格艾丝凡38 分钟前
两电平三相VSC整流模式从不控整流平滑切换至有源整流调试记录
算法·dsp·c2000
dongf20191 小时前
R语言KNN算法
算法·数据分析·r语言
小O的算法实验室1 小时前
2025年IEEE TASE,基于双层耦合平均场博弈的大规模智能体集成任务分配与轨迹规划
人工智能·算法·机器学习
8Qi81 小时前
LeetCode 337:打家劫舍 III(House Robber III)—— 题解 ✅
算法·leetcode·二叉树·动态规划
地平线开发者1 小时前
从 INT64 Div 算子约束到 Cast 修复全流程
算法
2601_961194021 小时前
教资科三美术考什么|初中高中美术题型考点和模板资料
leetcode·elasticsearch·职场和发展·蓝桥杯·pat考试·lucene
AI科技星1 小时前
基于奇合数边界的离散解析数论与双螺旋宇宙本体大统一体系论文全部数学公式汇总表
人工智能·算法·机器学习·架构·学习方法
地平线开发者2 小时前
Horizon 模型多 Batch 配置
算法·自动驾驶
czhaii2 小时前
GB2312简体中文编码表
单片机·算法