207. 课程表

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] 中的所有课程对 互不相同

📝 核心笔记:课程表 (拓扑排序 / DFS 找环)

1. 核心思想 (一句话总结)

"红绿灯机制:遇到'黄灯'就是追尾(环),遇到'红灯'就是安全。" 我们需要区分三种状态,防止重复计算并精准定位"回边"。

  • 0 (White/未访问):处女地,没去过。
  • 1 (Gray/正在访问) :当前递归栈中的节点(黄灯)。如果在递归中又遇到了 1,说明咬到了自己的尾巴 -> 有环!
  • 2 (Black/已完成):死胡同或已经确认安全的节点(红灯)。下次遇到直接跳过,不用再跑一遍。
2. 算法流程 (三步走)
  1. 建图 (Adjacency List) :把 [课程, 前置] 转化为 前置 -> 课程 的邻接表。
  2. 遍历所有节点 :图可能不是连通的,所以外层要用 for 循环遍历每一个节点。
  3. DFS (三色状态流转)
    • 进门 :标记为 1 (正在经手)。
    • 查邻居
      • 邻居是 1报警!有环!
      • 邻居是 0:递归进去查。
      • 邻居是 2:安全的,无视。
    • 出门 :标记为 2 (封存归档),表示从我出发的所有路都走通了且没环。
🔍 代码回忆清单 (带注释版)

✅ 举例:

假设 prerequisites = [[1,0], [2,0], [3,1]]

循环过程:

  1. p = [1, 0]g[0].add(1)g[0] = [1]
  2. p = [2, 0]g[0].add(2)g[0] = [1, 2]
  3. p = [3, 1]g[1].add(3)g[1] = [3]

最终邻接表:

复制代码
g[0] = [1, 2]   // 学完 0 可以学 1 或 2
g[1] = [3]      // 学完 1 可以学 3
g[2] = []       // 学完 2 没有后续课程
g[3] = []       // 学完 3 没有后续课程

// 题目:LC 207. Course Schedule
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 1. 建图 (邻接表)
        //创建一个数组,每个元素是一个列表
        List<Integer>[] g = new ArrayList[numCourses];
        //:给每个位置初始化一个空列表
        Arrays.setAll(g, i -> new ArrayList<>());
        for (int[] p : prerequisites) {
            // p[1] 是先修课,p[0] 是后修课
            // 边方向:先修 -> 后修
            // 从先修课 p[1] 指向后修课 p[0]
            g[p[1]].add(p[0]); 
        }

        // 2. 状态数组 (0:未访, 1:递归中, 2:已完成)
        int[] colors = new int[numCourses];
        
        // 3. 处理非连通图 (孤岛也要查)
        for (int i = 0; i < numCourses; i++) {
            // 只有没访问过的才进 DFS
            // 如果 dfs 返回 true,说明"找到了环",也就是"无法完成" -> return false
            if (colors[i] == 0 && dfs(i, g, colors)) {
                return false; 
            }
        }
        return true; // 全跑完了没发现环
    }

    // 返回值:true 表示"发现了环"
    private boolean dfs(int x, List<Integer>[] g, int[] colors) {
        colors[x] = 1; // 标记:我正在这条路径上 (入栈)

       // g[x] 是学完 x 后可以学的课程列表。
       // y 就是其中一个后续课程。
        for (int y : g[x]) {
            // 核心判断逻辑
            if (colors[y] == 1) {
                return true; // 撞见自己人/祖先了 -> 环!
            }
            if (colors[y] == 0 && dfs(y, g, colors)) {
                return true; // 递归子节点发现了环,层层上报
            }
            // colors[y] == 2 的情况直接跳过 (剪枝)
        }
        
        colors[x] = 2; // 标记:我和我的子孙都检查过了,没问题 (出栈)
        return false; // 平安无事
    }
}
⚡ 快速复习 CheckList (易错点)
  • \] **为什么不能只用 boolean visited?**

    • 普通的 visited 只能分"去过"和"没去过"。
    • 我们需要区分 :是"当前递归路径里遇到过 (环)"还是"别的路径已经验证过是安全的 (剪枝)"。这就是状态 12 的区别。
  • \] **返回值逻辑绕晕了?**

    • dfs 返回 true = 有坏人 (有环)
    • 主函数 if (dfs(...)) 成立,说明有环,课程表没法修完,所以主函数返回 false
    • 负负得正的逻辑要理清。
  • \] **边的方向?**

    • 通常题目给 [A, B] 表示修 A 必须先修 B。
    • 建图最好是 B -> A (学了 B 才能学 A)。这样如果有环,逻辑依然成立。
🖼️ 数字演练

假设依赖:0 -> 11 -> 0 (互为前置,死循环)。

  1. DFS(0)
    • Colors[0] = 1
    • 看邻居:1
    • 1 是颜色 0 -> 递归 DFS(1)
  1. DFS(1)
    • Colors[1] = 1
    • 看邻居:0
    • 关键点0 的颜色是 1!(说明 0 还在递归栈里,还没有变绿)。
    • Return True (发现环)。
  1. Back to DFS(0)
    • 收到子递归的 True
    • Return True
  1. Main
    • 收到 True (有环)。
    • Return False (不能完成)。
相关推荐
嵌入式×边缘AI:打怪升级日志1 小时前
9.2.1 分析 Write File Record 功能(保姆级讲解)
java·开发语言·网络
阿在在1 小时前
Spring 系列(三):Spring PostProcessor 顶级扩展接口全解析
java·后端·spring
Tisfy2 小时前
LeetCode 1523.在区间范围内统计奇数数目:两种方法O(1)算
算法·leetcode·题解
kyrie学java2 小时前
使用SpringBoot框架搭建简易的项目
java·spring boot·spring
癫狂的兔子2 小时前
【Python】【机器学习】线性回归
算法·回归·线性回归
野犬寒鸦2 小时前
ArrayList扩容机制深度解析(附时序图详细讲解)
java·服务器·数据结构·数据库·windows·后端
tankeven3 小时前
HJ92 在字符串中找出连续最长的数字串
c++·算法
逆境不可逃3 小时前
【从零入门23种设计模式03】创建型之建造者模式(简易版与导演版)
java·后端·学习·设计模式·职场和发展·建造者模式
小雨中_3 小时前
3.1 RLHF:基于人类反馈的强化学习
人工智能·python·深度学习·算法·动态规划