207. 课程表

207. 课程表

中等

提示

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 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 只能分"去过"和"没去过"。
    • 我们需要区分 :是"当前递归路径里遇到过 (环)"还是"别的路径已经验证过是安全的 (剪枝)"。这就是状态 1 和 2 的区别。
  • 返回值逻辑绕晕了?
    • dfs 返回 true = 有坏人 (有环)。
    • 主函数 if (dfs(...)) 成立,说明有环,课程表没法修完,所以主函数返回 false。
    • 负负得正的逻辑要理清。
  • 边的方向?
    • 通常题目给 [A, B] 表示修 A 必须先修 B。
    • 建图最好是 B -> A (学了 B 才能学 A)。这样如果有环,逻辑依然成立。
🖼️ 数字演练

假设依赖:0 -> 1,1 -> 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 (不能完成)。
相关推荐
小羊没烦恼!3 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
倒头就睡的小比特3 天前
算法竞赛C++常用的STL
c++·算法
俊昭喜喜里3 天前
java中的继承和多态的区别
java
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕3 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码3 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖3 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时3 天前
01-06-A-JVM排查实战详解
java·jvm
猎头南楼3 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
vipxieliang3 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot