207. 课程表(Course Schedule)

难度:中等 | 主题:图论、拓扑排序、DFS、BFS

题目

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses-1。在选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] 表示如果要学习课程 ai 则必须先学习课程 bi。请你判断是否可能完成所有课程的学习。

示例

 
输入:numCourses = 2, prerequisites = 1,0
 
输出:true
 
解释:先修关系 01,可以完成
 

思路

先讲个故事:选课系统的死循环

教务系统让你选课,但有些课有先修要求:A 课要先修 B 课,B 课要先修 C 课……

如果 A→B→C→A 形成死循环——永远选不完

怎么检测死循环?拓扑排序


引导式推导:两种判环方法

第 1 层:BFS 拓扑排序(Kahn 算法,推荐)


flowchart TD

    A["建图 + 计算入度"] --> B["入度为 0 的节点入队"]

    B --> C["出队一个节点"]

    C --> D["后继节点入度 -1"]

    D --> E{"入度变 0?"}

    E -->|"是"| F["入队"]

    E -->|"否"| G["继续"]

    F --> H{"队列空?"}

    G --> H

    H -->|"否"| C

    H -->|"是"| I{"处理节点数 == 总课程数?"}

    I -->|"是"| J["无环 ✓"]

    I -->|"否"| K["有环 ✗"]

第 2 层:DFS 判环(三色标记法)


graph TD

    A["0: 未访问"] -->|"DFS 进入"| B["1: 正在访问"]

    B -->|"DFS 完成"| C["2: 已访问"]

    B -->|"遇到 1"| D["有环!"]


两种方法对比

维度BFS(Kahn)DFS(三色标记)
直观性更直观,入度为 0 先处理需要理解三种状态
代码量稍多稍少
输出拓扑序天然输出需要额外记录
适用场景需要拓扑序时只判环时

代码

 
from collections import deque, defaultdict
 
# BFS 拓扑排序(Kahn 算法,推荐)
 
class Solution:
 
    def canFinish(self, numCourses: int, prerequisites: list[list[int]]) -> bool:
 
        graph = defaultdict(list)
 
        indegree = [0] * numCourses
 
        for a, b in prerequisites:
 
            graph[b].append(a)
 
            indegree[a] += 1
 
        queue = deque()
 
        for i in range(numCourses):
 
            if indegree[i] == 0:
 
                queue.append(i)
 
        count = 0
 
        while queue:
 
            course = queue.popleft()
 
            count += 1
 
            for next_course in graph[course]:
 
                indegree[next_course] -= 1
 
                if indegree[next_course] == 0:
 
                    queue.append(next_course)
 
        return count == numCourses
 
 
# DFS 判环
 
class Solution:
 
    def canFinish(self, numCourses: int, prerequisites: list[list[int]]) -> bool:
 
        graph = defaultdict(list)
 
        for a, b in prerequisites:
 
            graph[b].append(a)
 
        state = [0] * numCourses
 
        def has_cycle(node: int) -> bool:
 
            if state[node] == 1:
 
                return True
 
            if state[node] == 2:
 
                return False
 
            state[node] = 1
 
            for next_node in graph[node]:
 
                if has_cycle(next_node):
 
                    return True
 
            state[node] = 2
 
            return False
 
        for i in range(numCourses):
 
            if state[i] == 0:
 
                if has_cycle(i):
 
                    return False
 
        return True
 

复杂度

指标解释
时间O(V+E)建图 + 拓扑排序
空间O(V+E)图的存储 + 入度/状态数组

实战考量

频率分析

出现在:字节/阿里 高频题,约 35% 常会考到。考察 DAG 和环的判断

延伸思考

Q:BFS 和 DFS 判环有什么区别?

A:BFS(Kahn)更直观,适合找拓扑序;DFS 三种状态法代码简洁。

Q:如果要求输出一个合法的上课顺序呢?

A:210课程表II,拓扑排序的过程中记录顺序。

Q:如果有多个合法顺序,怎么保证输出某个特定顺序?

A:用优先队列代替普通队列,每次选编号最小的。

Q:并查集能做吗?

A:可以检测环,但不能给出拓扑序。

易错点

  • 建图方向别反:b -> a 表示 b 是 a 的先修课

  • BFS 最后比较 count == numCourses

  • DFS 状态标记:进入时设 1,退出时设 2


生活类比

选课系统的死循环检测

教务系统要检查先修关系有没有死循环。

Kahn 算法的思路:先选没有先修要求的课(入度为 0),选完后把依赖它的课的先修数 -1。

如果所有课都能选完,说明没死循环;选不完就是有环。

入度为 0 = 没有先修 = 可以直接选——这是拓扑排序的核心。


相关题目

题目关系
210课程表II输出拓扑序
684冗余连接无向图判环(并查集)
547省份数量连通块计数
207课程表本题

→ 返回题单:LeetCode学习路线图 > 十二、图论

速记卡(面试闪卡)

Q1:一句话讲清「207. 课程表(Course Schedule)」到底是什么?

A:《课程表》判断选课先修关系有无环,拓扑排序(Kahn/DFS)即可判定。

Q2:题目 —— 怎么理解?

A:像查选课死循环:给 numCourses 与先修对 [ai,bi],能否学完所有课;题目(Problem)本质是有向图判环。

Q3:思路 —— 怎么理解?

A:像拆先修链条:BFS(Kahn)入度 0 先选、减后继入度;DFS 三色标记(白/灰/黑)遇灰即环;拓扑排序(Topological Sort)检测环。

Q4:代码 —— 怎么理解?

A:建图 b→a、入度统计,队列装入度 0 节点,出队减后继入度、归零入队;代码(Code)最后 count==numCourses 即无环。

Q5:复杂度 —— 怎么理解?

A:像扫一遍图:时间 O(V+E) 建图加拓扑,空间 O(V+E) 存图与数组;复杂度(Complexity)随课程数 V 与先修边 E。

Q6:核心速记主线有哪些?

  • 题目:先修关系能否学完,本质判环

  • 思路:Kahn 入度 0 出队 / DFS 三色标记

  • 代码:建图统计入度,count==numCourses

  • 复杂度:时间 O(V+E),空间 O(V+E)

口诀

A:课程表判环,

入度零先选;

拓扑排一遍,

选完即无环。

相关链接