当前:LC210 · 课程表 II / Course Schedule II · 首次出现于 Day 31 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC210 · Course Schedule II · 图 / 拓扑排序

课程表 II:拓扑序就是修课顺序,有环交空数组

先修关系是有向边。入度为 0 的课可以上;出队时写入 order。修不完 numCourses 门,说明有环,返回 []。

边 b→a 表示修 a 之前必须修 b。统计入度,入度 0 入队。出队把课追加到 order,并把后继入度减一,减到 0 再入队。order 长度等于 numCourses 则返回它,否则返回空数组。时间 O(V+E),空间 O(V+E)。

时间 O(V+E)空间 O(V+E)结论先行 · 全文约 6 节
导读

这是 LeetCode 210. Course Schedule II。共有 numCourses 门课,编号 0 到 n−1。prerequisites[i] = [ai, bi] 表示修 ai 之前必须先修 bi。返回任意一个能修完所有课的顺序;不可能则返回空数组。

主例 numCourses = 4,先修 [[1,0],[2,0],[3,1],[3,2]]。0 没有先修;1 和 2 都等 0;3 等 1 和 2。一条合法顺序是 [0,1,2,3]。1 和 2 谁先谁后都行。

第一直觉是「按编号从小到大上课」。主例碰巧 0、1、2、3 都合法,但若先修是 [0,1],编号序会先上 0,先修没满足。缺的是动态维护「现在谁的先修都修完了」。下面从这块缺口推出 Kahn 拓扑排序。

出队写入 order,写不满就是环

先修 [a,b] 是有向边 b→a:b 修完,a 的入度才能减。图画反了,入度会记到先修课头上,顺序全错。入度数组长度是课程数,不是先修条数。

缺的信息是「此刻可以上的课」。没有入度,你不知道谁的门已经打开。有入度却不出队记录,就只剩 LC207 的布尔判断。本题要把每次出队的课追加到 order:它当前没有未完成的先修,放进序列一定合法。

从缺口写出 Kahn。建邻接表和入度。所有入度 0 的课入队。当队列非空:弹出 cur,写入 order;对 graph[cur] 里每个后继,入度减一,减到 0 就入队。循环结束看长度。少一门,就是有人入度永远减不完——环。环上的课互相等待,返回 []。

用手走主例。入度 [0,1,1,2],只有课 0 就绪,ready=[0],taken 空。演示第一帧。修完 0,1 和 2 的入度变成 0,3 仍是 2。order=[0],ready=[1,2]。演示第二帧。1、2 先后出队(队列顺序决定谁先),3 的入度两次减一变成 0。order=[0,1,2],ready=[3]。演示第三帧。3 出队,order=[0,1,2,3],长度 4,返回它。

若再加一条 [0,3],0 和 3 互相等待,队列会在某步空掉而 order 写不满,返回 []。合法顺序不唯一,测试只要求拓扑约束成立。每条边减一次入度,时间 O(V+E)。DFS 三色法也可以:后序反转得到拓扑序,碰到回边就是环,同样返回 []。

空数组不是失败兜底,是有环order 长度不够,说明存在入度减不完的课。不要返回「已经排出的前缀」——前缀合法,但题目要的是修完所有课。
4 门课
0入度 0
1入度 1
2入度 1
3入度 2
入度 0:课 0

Go:Kahn 输出顺序

solution.goGo
func findOrder(numCourses int, prerequisites [][]int) []int {
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) }
}
order := []int{}
for len(q) > 0 {
cur := q[0]; q = q[1:]
order = append(order, cur)
for _, nb := range graph[cur] {
indegree[nb]--
if indegree[nb] == 0 { q = append(q, nb) }
}
}
if len(order) != numCourses { return []int{} }
return order
}

1边从 p[1] 指向 p[0]:先修课指向后修课。入度加在后修课上。

2入度 0 的课先入队。主例一开始只有 0。

3出队立刻写入 order。主例三次出队后得到 [0,1,2],再收下 3。

4长度对不上就返回空切片。不要返回半截 order。

总结

Kahn 出队顺序就是修课序。主例 [0,1,2,3];写不满 numCourses 就返回 []。

  • [a,b] 是 b→a,不是 a→b。图画反则入度全错。
  • 1 和 2 谁先出队都合法,只要 0 在它们前、3 在它们后。
  • 有环时队列会空、order 偏短。空数组是「修不完」,不是「没有先修」。
同族题目
LC207课程表LC269火星词典LC310最小高度树