Leetcode 153 课程表 | 腐烂的橘子

序:图论模板

模板一:岛屿数量(DFS - 洪水填充)

核心记忆:看到一个'1',就把它和周围连着的所有'1'都变成'2'(表示访问过),然后岛屿数+1。

cpp 复制代码
class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty() || grid[0].empty()) return 0;
        
        int rows = grid.size();
        int cols = grid[0].size();
        int count = 0;
        
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == '1') {
                    dfs(grid, i, j, rows, cols);
                    count++;  // 发现一块陆地,整片岛屿都标记完了,计数+1
                }
            }
        }
        return count;
    }
    
private:
    void dfs(vector<vector<char>>& grid, int i, int j, int rows, int cols) {
        // 退出条件:越界 或者 不是陆地
        if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] != '1') {
            return;
        }
        
        grid[i][j] = '2';  // 标记已访问(改成'2'或'0'都行)
        
        // 四个方向递归
        dfs(grid, i + 1, j, rows, cols);
        dfs(grid, i - 1, j, rows, cols);
        dfs(grid, i, j + 1, rows, cols);
        dfs(grid, i, j - 1, rows, cols);
    }
};

背法口诀:两层循环找'1',找到就调DFS;DFS里先判断,越界非'1'就返回;改值标记然后递归四个方向。


模板二:腐烂的橘子(多源BFS)

核心记忆:先把所有坏橘子放入队列,然后一层一层往外传播。

for(int i = queue.size(); i > 0; i--)来控制层数(分钟)。

cpp 复制代码
class Solution {
public:
    int orangesRotting(vector<vector<int>>& grid) {
        int rows = grid.size();
        int cols = grid[0].size();
        queue<pair<int, int>> q;
        int fresh = 0;
        
        // 第一步:把所有坏橘子入队,同时统计好橘子数量
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == 2) {
                    q.push({i, j});
                } else if (grid[i][j] == 1) {
                    fresh++;
                }
            }
        }
        
        if (fresh == 0) return 0;  // 没有好橘子,直接返回0
        
        int minutes = 0;
        int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};  // 四个方向
        
        // 第二步:BFS层序遍历
        while (!q.empty() && fresh > 0) {
            minutes++;
            int size = q.size();  // 关键:记录当前层的橘子数
            for (int i = 0; i < size; i++) {
                auto [r, c] = q.front();
                q.pop();
                
                for (auto& dir : dirs) {
                    int nr = r + dir[0];
                    int nc = c + dir[1];
                    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
                        grid[nr][nc] = 2;
                        fresh--;
                        q.push({nr, nc});
                    }
                }
            }
        }
        
        return fresh == 0 ? minutes : -1;
    }
};

背法口诀:先遍历,坏入队好计数;层数用size锁,四个方向去传染;传染完fresh减,最后看fresh归零没。


模板三:课程表(拓扑排序 / Kahn算法)

核心记忆

建图(邻接表)+ 入度数组 -> 入度为0的入队 -> BFS弹出时减少邻居入度 -> 统计弹出的节点数是否等于总课程数。

cpp 复制代码
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        // 1. 建图:邻接表 和 入度数组
        vector<vector<int>> graph(numCourses);
        vector<int> indegree(numCourses, 0);
        
        for (auto& pre : prerequisites) {
            int course = pre[0];
            int prerequisite = pre[1];
            graph[prerequisite].push_back(course);  // prerequisite -> course
            indegree[course]++;
        }
        
        // 2. 将所有入度为0的节点入队
        queue<int> q;
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                q.push(i);
            }
        }
        
        // 3. BFS处理
        int count = 0;
        while (!q.empty()) {
            int node = q.front();
            q.pop();
            count++;
            
            for (int neighbor : graph[node]) {
                indegree[neighbor]--;
                if (indegree[neighbor] == 0) {
                    q.push(neighbor);
                }
            }
        }
        
        return count == numCourses;
    }
};

背法口诀:遍历先决条件建图算入度;入度为零入队列;弹出节点count加,邻居入度减减看归零;最后看count是否等于总课程。

拓扑排序算法

环检测算法

图结构的 DFS/BFS 遍历

1 题目

207. 课程表

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai必须 先学习课程 bi

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

示例 1:

复制代码
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

复制代码
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

2 代码实现

c++

cpp 复制代码
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        vector <vector <int >> graph (numCourses);
        vector <int> indegree (numCourses ,0 );

        for (auto & pre : prerequisites){
            int course = pre[0];
            int prerequisites = pre[1] ;
            graph[prerequisites].push_back(course);
            indegree[course] ++;
        }

        queue <int> q ;
        for (int i = 0 ; i < numCourses ; i ++){
            if (indegree[i] == 0 ){
                q.push(i);
            }
        }

        int count = 0 ;
        while (!q.empty()){
            int node = q.front() ;
            q.pop();
            count ++ ;

            for (int neighbor : graph[node]){
                indegree[neighbor] -- ;
                if (indegree [neighbor] == 0 ){
                    q.push(neighbor);
                }
            }
        }
        return count == numCourses ;
    }
};

js

javascript 复制代码
/**
 * @param {number} numCourses
 * @param {number[][]} prerequisites
 * @return {boolean}
 */
var canFinish = function(numCourses, prerequisites) {
    const graph = new Array(numCourses).fill().map(() => []);
    const indegree = new Array (numCourses).fill(0);

    for (const pre of prerequisites){
        const course = pre [0];
        const prerequisites = pre[1];

        graph[prerequisites].push(course);

        indegree[course]++;
    }
        const queue = [];

        for (let i = 0 ; i < numCourses ; i++){
            if (indegree[i] === 0 ){
                queue.push(i);
            }
        }

        let count = 0 ;

        while (queue.length > 0 ){
            const node = queue.shift();
            count ++;

            for (const neighbor of graph[node]){
                indegree[neighbor]--;

                if (indegree[neighbor] === 0 ){
                    queue.push(neighbor);
                }
            }
        }
        return count === numCourses ;
};

思考

这怎么做啊???????我也许只能先背诵一下。。。。

题解

cpp 复制代码
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        // graph[课] = 学完这门课之后能解锁的课程列表
        // 例如:graph[0] = [1,2] 表示学完0之后,1和2的前置条件少了一个
        vector<vector<int>> graph(numCourses);
        
        // indegree[课] = 这门课还有多少个前置条件没完成
        // 例如:indegree[3] = 2 表示学3之前还需要先学2门别的课
        vector<int> indegree(numCourses, 0);

        // 遍历所有的先决条件,把图建好,把入度算好
        for (auto& pre : prerequisites) {
            int course = pre[0];        // 想学的课
            int prerequisite = pre[1];  // 必须先学的课
            
            // 从 prerequisite 指向 course:学完 prerequisite 后可以解锁 course
            graph[prerequisite].push_back(course);
            
            // course 又多了一个前置条件,所以入度+1
            indegree[course]++;
        }

        // 队列里存放"当前可以学的课"(也就是入度为0的课)
        queue<int> q;
        
        // 把所有入度为0的课(没有前置条件的课)加入队列
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                q.push(i);
            }
        }

        // count 记录已经学完的课程数量
        int count = 0;
        
        // 每次从队列里取一门课来"学"
        while (!q.empty()) {
            int node = q.front();  // 当前要学的课
            q.pop();
            count++;  // 学完了,计数+1
            
            // 学完 node 之后,看看它能解锁哪些课
            for (int neighbor : graph[node]) {
                // neighbor 这门课的前置条件少了一个(因为 node 学完了)
                indegree[neighbor]--;
                
                // 如果 neighbor 的前置条件全部学完了(入度变成0)
                // 那么 neighbor 现在可以学了,加入队列
                if (indegree[neighbor] == 0) {
                    q.push(neighbor);
                }
            }
        }
        
        // 如果学完的课程数量等于总课程数,说明没有环,可以全部学完
        // 如果小于,说明有环(互相依赖),有些课永远学不了
        return count == numCourses;
    }
};
bash 复制代码
输入:numCourses = 4, prerequisites = [[1,0], [2,0], [3,1], [3,2]]

执行过程:

初始:
indegree = [0, 1, 1, 2]   // 课0没人依赖它,课3有两门前置课
queue = [0]                // 课0可以学

第1步:学课0 → count=1 → 解锁课1和课2 → indegree变成[0,0,0,2] → 课1、课2入队
queue = [1, 2]

第2步:学课1 → count=2 → 解锁课3 → indegree变成[0,0,0,1]
queue = [2]

第3步:学课2 → count=3 → 解锁课3 → indegree变成[0,0,0,0] → 课3入队
queue = [3]

第4步:学课3 → count=4
queue = []

count(4) == numCourses(4) → 返回 true
cpp 复制代码
numCourses = 4
prerequisites = [[1,0], [2,0], [3,1], [3,2]]
  • 课程:0、1、2、3
  • 依赖关系:
    • 学 1 前先学 0
    • 学 2 前先学 0
    • 学 3 前先学 1 和 2

有向图结构:

复制代码
      0
     / \
    1   2
     \ /
      3

预期结果:true

第 1 步:建图 + 统计入度

  • 邻接表

    cpp 复制代码
    graph[0] = [1, 2]
    graph[1] = [3]
    graph[2] = [3]
    graph[3] = []
  • 入度数组

    cpp 复制代码
    indegree[0] = 0
    indegree[1] = 1
    indegree[2] = 1
    indegree[3] = 2

第 2 步:入度为 0 的节点入队

cpp 复制代码
队列 q = [0]
已修课程计数 count = 0

第 3 步:BFS 遍历

第 1 轮
  • 弹出节点 0,count = 1
  • 处理邻居 1:indegree[1]-- → 0,入队
  • 处理邻居 2:indegree[2]-- → 0,入队
  • 队列:[1, 2]
第 2 轮
  • 弹出节点 1,count = 2
  • 处理邻居 3:indegree[3]-- → 1,不入队
  • 队列:[2]
第 3 轮
  • 弹出节点 2,count = 3
  • 处理邻居 3:indegree[3]-- → 0,入队
  • 队列:[3]
第 4 轮
  • 弹出节点 3,count = 4
  • 无邻居,队列变空

第 4 步:结果判断

count == numCourses → 4 == 4 → return true

cpp 复制代码
初始入度:[0,1,1,2],队列:[0]

1. 弹出 0 → 1、2 入度归零 → 入队
   入度:[0,0,0,2],队列:[1,2]

2. 弹出 1 → 3 入度变为 1
   入度:[0,0,0,1],队列:[2]

3. 弹出 2 → 3 入度归零 → 入队
   入度:[0,0,0,0],队列:[3]

4. 弹出 3 → 全部学完
count = 4,返回 true
cpp 复制代码
numCourses = 2
prerequisites = [[1,0], [0,1]]
  • 0 和 1 互相依赖,形成环
  • 初始入度:[1,1],无入度为 0 节点
  • 队列为空,BFS 不执行,count = 0
  • 0 == 2 → return false

环上节点入度永远无法变为 0,不会入队,最终 count < 总课程数,以此判断有环、无法完成。

3 题目

994. 腐烂的橘子

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

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

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -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
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。

示例 3:

复制代码
输入:grid = [[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 10
  • grid[i][j] 仅为 012

4 代码实现

c++

cpp 复制代码
class Solution {
public:
    int orangesRotting(vector<vector<int>>& grid) {
        int rows = grid.size(); // 行
        int cols = grid[0].size(); //列
        
        queue<pair<int, int >> q ;
        int fresh = 0 ;

        for (int i = 0 ; i < rows ; i++){
            for (int j = 0 ; j < cols ; j ++){
                if (grid[i][j] == 2 ){
                    q.push({i , j});
                }else if (grid[i][j] == 1 ){
                    fresh ++ ;
                }
            }
        }
        if (fresh == 0 ) return 0 ; 

        int minutes = 0 ;
        int dirs[4][2] = {{1, 0},{-1, 0},{ 0, 1},{0, -1}};

        while (!q.empty()&& fresh > 0){
            minutes ++ ;
            int s = q.size();
            for (int i = 0 ; i < s; i++){
                auto[r,c] = q.front();
                q.pop();

                for (auto& dir :dirs){
                    int nr = r + dir[0];
                    int nc = c + dir[1];
                    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
                        grid[nr][nc] = 2;
                        fresh--;
                        q.push({nr, nc});
                    }
                }
            }
        }
        return fresh == 0 ? minutes : -1 ;
    }
};

js

javascript 复制代码
/**
 * @param {number[][]} grid
 * @return {number}
 */
var orangesRotting = function(grid) {
    const rows = grid.length;
    const cols = grid[0].length ;

    const queue = [];
    let fresh = 0 ;

    for (let i = 0 ; i < rows ; i ++){
        for (let j = 0 ; j < cols ; j++){
            if (grid[i][j] === 2 ){
                queue.push([i,j]);
            }else if (grid[i][j] === 1){
                fresh ++ ;
            }
        }
    }

    if (fresh === 0 ) return 0 ;

    let minutes = 0 ; 
    const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];

    while (queue.length > 0 && fresh > 0){
       minutes ++;
       const levelSize = queue.length ;

       for (let i = 0 ; i < levelSize ; i++){
        const [r ,c ] = queue.shift ();

        for (const dir of dirs){
            const nr = r + dir[0];
            const nc = c + dir[1];

            if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1){
                grid[nr][nc] = 2 ;
                fresh -- ;
                queue.push([nr,nc]);
            }
          }
       } 
    }  
    return fresh === 0 ? minutes : -1 ;
};

思考

处理思路,如果是腐烂的橘子特殊处理,bfs四周扩散。

自己抄还抄错了

cpp 复制代码
class Solution {
public:
    int orangesRotting(vector<vector<int>>& grid) {
        int rows = grid.size(); // 行
        int cols = grid[0].size(); //列
        
        queue<pair<int, int >> q ;
        int fresh = 0 ;

        for (int i = 0 ; i < rows ; i++){
            for (int j = 0 ; j < cols ; j ++){
                if (grid[i][j] == 2 ){
                    q.push({i , j});
                }else if (grid[i][j] == 1 ){
                    fresh ++ ;
                }
            }
        }
        if (fresh == 0 ) return 0 ; 

        int minutes = 0 ;
        int dirs[4][2] = {{1, 0},{-1, 0},{ 0, 1},{0, -1}};

        while (!q.empty()&& fresh > 0){
            minutes ++ ;
            for (int i = 0 ; i <  q.size() ; i++){
                auto[r,c] = q.front();
                q.pop();

                for (auto& dir :dirs){
                    int nr = r + dir[0];
                    int nc = c + dir[1];
                    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
                        grid[nr][nc] = 2;
                        fresh--;
                        q.push({nr, nc});
                    }
                }
            }
        }
        return fresh == 0 ? minutes : -1 ;
    }
};

错因

问题在于 while 循环里的 for 循环条件使用了动态变化的 q.size()

好老掉牙的错误。哭了。

5 小结

我说实话我现在只能背诵。。。。为何会如此,好难啊!手敲了一遍什么都没过脑子。。。感觉图学得完蛋了。

bash 复制代码
// 岛屿
numIslands() {
    for 遍历所有格子
        if 是陆地
            dfs标记整片
            count++
}

// 腐烂橘子
orangesRotting() {
    统计坏橘子和好橘子
    while(队列不空 && 有好橘子)
        minutes++
        int size = q.size()
        for 处理这一层所有坏橘子
            四个方向传染
}

// 课程表
canFinish() {
    建图、统计入度
    入度为0的入队
    while(队列不空)
        count++
        for 每个邻居
            indegree[邻居]--
            if 变成0 then 入队
    return count == numCourses
}

但是重新ai陪我看一轮我发现图论也没那么难,把一行一行代码看清楚就行了。

加油,坚持,也许睡梦中会复盘。。哈哈!

相关推荐
paeamecium2 小时前
【PAT甲级真题】- Reversing Linked List (25)
数据结构·c++·算法·pat
田梓燊2 小时前
leetcode 73
算法·leetcode·职场和发展
烈风2 小时前
01_Tauri环境搭建
开发语言·前端·后端
cch89182 小时前
PHP爬虫框架大比拼
开发语言·爬虫·php
ZPC82102 小时前
相机接入ROS2 流程及问题排查
人工智能·算法·机器人
2501_940315262 小时前
【无标题】两个相同字符串中不同字符的个数
算法·哈希算法·散列表
l1t2 小时前
DeepSeek辅助编写的dmp转schema和csv文件c语言程序
c语言·开发语言·windows
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 54. 螺旋矩阵 | C++ 模拟法题解
c++·leetcode·矩阵
DREW_Smile2 小时前
自定义类型:联合体和枚举
c语言·开发语言