课程表:先修图里有环吗
能学完所有课,当且仅当先修图是有向无环图。Kahn 用入度 0 的队列把这句话跑出来。
把 prerequisites[i] = [ai, bi] 建成有向边 bi → ai,统计每门课的入度。入度为 0 的课没有未完成的先修,入队学习;学完一门,把后修入度 −1,减到 0 的继续入队。学完数量等于 numCourses 则可学完,否则剩下的课困在环里。时间 O(V+E),空间 O(V+E)。
这是 LeetCode 207. Course Schedule。大白话:一共 numCourses 门课,编号从 0 到 n−1。prerequisites[i] = [ai, bi] 表示学 ai 之前必须先学 bi。问这些课能不能全部修完。
主例只用 2 门课、一条先修 [[1, 0]]:先学 0,再学 1,能学完,返回 true。对照环:[[1, 0], [0, 1]],0 等 1、1 等 0,谁也开不了头,返回 false。边的方向先说死:本篇画成 bi → ai,学完 b 才能解锁 a。
没有「入度为 0 的先学」这张清单,模拟会碰运气:先挑 1,先修 0 还没学,就卡住。缺的不是再写一层递归,而是一个可执行的停机条件——队列空了之后,学完的数量够不够 n。够就是有拓扑序,不够就是有环。检测环和拓扑排序是同一件事的两种说法。
入度 0 先进队
输入给的是边表,不是画好的图。第一步建邻接表,顺手统计入度:每条 [ai, bi] 往 graph[bi] 里追加 ai,同时 indegree[ai] +1。方向和入度必须一致;画反了,队列里先出来的就不是「没有先修的课」。
一门课的入度 = 还有几门先修没学。入度为 0,前置清单是空的,现在就可以学。可能同时有好几门,彼此没有依赖,谁先学都可以。把它们放进队列,不是因为最短路,是因为「当前可学集合」需要一个容器。
出队就是修完。学完课 u,所有 u → v 的边作废,v 的入度 −1。某门课减到 0,说明它的先修刚刚齐,进入可学集合。继续直到队列空。最后数 taken:等于 numCourses,每门课都有过入度为 0 的时刻,能学完;小于 n,剩下的课入度永远减不完,彼此还指着对方,那就是环。不必把环的节点列出来,个数对不上已经够否决。
主例 n = 2,边 0 → 1。入度是 [0, 1],队列一开始只有课 0。修完 0,taken = 1,课 1 的入度从 1 减成 0,入队。修完 1,taken = 2。队列空,2 == 2,返回 true。顺序只能是 0 再 1。
环例两条边 0 → 1、1 → 0,入度都是 1。没有入度为 0 的点,队列一开始就是空的,taken = 0,不等于 2,返回 false。环上每个点入度至少 1,减入度的动作永远发动不起来。再记一句:有的课能学,不等于能学完。三门课若只有 0 → 1、1 → 2、2 → 1,课 0 能学,1 和 2 互相指着,taken 停在 1,本题问的是后者。
Kahn 算法拓扑的贪心是「永远先学没有前置的课」。队列空不是成功,taken == n 才是。有环时队列会提前耗尽,剩下的入度减不完。
Go:Kahn 拓扑
func canFinish(numCourses int, prerequisites [][]int) bool {graph := make([][]int, numCourses)indegree := make([]int, numCourses)for _, p := range prerequisites {graph[p[1]] = append(graph[p[1]], p[0])indegree[p[0]]++}q := []int{}for i := 0; i < numCourses; i++ {if indegree[i] == 0 { q = append(q, i) }}taken := 0for len(q) > 0 {cur := q[0]; q = q[1:]taken++for _, nb := range graph[cur] {indegree[nb]--if indegree[nb] == 0 { q = append(q, nb) }}}return taken == numCourses}
1graph[p[1]] 追加 p[0],边是 bi → ai;indegree[p[0]]++ 与这个方向绑定,画反则整张表反号。
2先把入度为 0 的课全部入队。它们是当前可学集合,可能有多门,彼此谁先都行。
3出队即修完:taken +1,再把它指出去的后修入度 −1。减到 0 的课,先修刚齐,入队。
4循环结束看 taken == numCourses。队列空不是成功——环例的队列一开始就空,靠这一句否决。
总结
入度为 0 的先学,学完减后修入度;学不完的那些,就是环。
- 能学完 ≡ 存在拓扑序 ≡ 有向无环。Kahn 把这句话收成队列和入度,不必单独再写一个「判环」。
- 队列空只说明暂时没人可学;taken 必须等于总课数。环上每个点入度至少 1,减入度发动不起来。
- LC210 要一份具体顺序:同一段 Kahn,把出队的课记下来就是一份合法拓扑序。本题只问存在性。