207. 课程表
LeetCode 2071 · 中等 · 图论
题
numCourses 门课编号 0 到 numCourses - 1,prerequisites[i] = [a, b] 表示修 a 之前必须先修完 b。问能不能把所有课修完。
例:2 门课,[[1,0]] → true;[[1,0],[0,1]] → false(互为前置)。
一句话
把依赖建成有向图 b → a,不断修掉「没有未完成前置」(入度为 0)的课;最后修掉的门数等于总数就能修完。
关键技巧
转成图论问题:修得完 ⇔ 依赖图无环。 有环时环上每门课都等着另一门,谁都修不了;无环时一定存在拓扑序,按序修即可。
Kahn 算法:入度为 0 的点可以直接修。 维护每个点的入度(还剩几门前置没修)。入度为 0 的全入队;每弹出一门课,它的所有后继入度减一,减到 0 的入队。
为什么「剥不完」就说明有环。 环上的每个点都有一条来自环内的入边,只要环上还没有任何点被剥掉,这些入边就一直在,入度永远不为 0——所以环上的点一个都进不了队。反过来,无环图里剩下的点构成的子图也无环,必有入度为 0 的点,过程不会卡住。修掉的数量 == numCourses 恰好等价于无环。
DFS 三色法是另一种写法:0 未访问、1 在当前递归栈上、2 已确认安全。DFS 时遇到状态 1 的邻居,就是沿路径绕回了栈上的点——有环。
解
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph = [[] for _ in range(numCourses)]
for a, b in prerequisites:
graph[b].append(a)
state = [0] * numCourses # 0 未访问,1 在栈上,2 已确认无环
def has_cycle(u):
state[u] = 1
for v in graph[u]:
if state[v] == 1 or (state[v] == 0 and has_cycle(v)):
return True
state[u] = 2
return False
return not any(state[u] == 0 and has_cycle(u) for u in range(numCourses))
时间 \(O(V + E)\),空间 \(O(V + E)\)。链状依赖的递归深度可达 \(V\)(最多 2000),本地跑可能要 sys.setrecursionlimit。
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph = [[] for _ in range(numCourses)]
indeg = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a)
indeg[a] += 1
q = deque(u for u in range(numCourses) if indeg[u] == 0)
done = 0
while q:
u = q.popleft()
done += 1
for v in graph[u]:
indeg[v] -= 1
if indeg[v] == 0: # 前置全部修完
q.append(v)
return done == numCourses # 剥不完就有环
时间 \(O(V + E)\),空间 \(O(V + E)\)。
延伸
- 要输出一个修课顺序:210. 课程表 II2,Kahn 里出队顺序就是答案;DFS 版是后序(状态变 2 的顺序)反转。
- 无向图判环不能用入度,改用并查集或 DFS 时跳过父节点。
- 同样按层推进的 BFS:994. 腐烂的橘子;网格图的连通分量:200. 岛屿数量。
- 坑:边的方向。
[a, b]是「b 是 a 的前置」,边从b指向a;反过来建图,判环结论不变,但到 210 要输出顺序时就反了。
自测
s = Solution()
assert s.canFinish(2, [[1, 0]]) is True
assert s.canFinish(2, [[1, 0], [0, 1]]) is False
assert s.canFinish(1, []) is True
assert s.canFinish(3, [[0, 0]]) is False # 自环
assert s.canFinish(4, [[1, 0], [2, 1], [3, 2], [1, 3]]) is False
assert s.canFinish(5, [[1, 0], [2, 0], [3, 1], [3, 2], [4, 3]]) is True
assert s.canFinish(2000, [[i + 1, i] for i in range(1999)]) is True # 长链
-
LeetCode 207,2026-09-29 读。 ↩
-
210. 课程表 II,2026-09-29 读。 ↩