算法困惑问答 · LC 207 课程表
课程表怎么判断课程能不能修完?为什么是判环?
直接答案
把每条先修关系「修 b 之前要先修 a」建成有向边 a → b,问题就变成:这张有向图有没有环。无环,总能找到一个合法的修课顺序(拓扑序);有环,环上的课互为前提、死锁,谁也开不了头。判环用 Kahn 算法:入度为 0 的课先修,修完给它指向的课减入度,减到 0 就入队;最后「修掉的课数等于总课数」即无环,能修完。
为什么「有环 = 修不完」
假设 A 的先修是 B、B 的先修是 C、C 的先修又是 A:想修 A 得先修 B,想修 B 得先修 C,想修 C 又绕回 A——三门课互相等待,永远没有一门能开始。这就是环的含义:依赖关系兜了一圈回到自己。反过来,图无环时总存在「没有任何先修要求」的课(入度 0),从它开始剥洋葱,一层层总能剥完。
Kahn 算法就是把「剥洋葱」写成代码:① 建邻接表、统计每门课的入度;② 入度为 0 的全部入队;③ 出队一门课记为「修掉」,它指向的每门课入度减 1,减到 0 就入队;④ 循环到队列空。
判断依据是计数,不是队列空
队列空了直接返回 true 是本题最经典的 bug。有环时,环上的课入度永远大于 0、从头到尾进不了队,队列会「提前」空掉——课没修完,循环却正常结束了。所以必须数「修掉了几门」,最后和总课数比较:相等才是无环。
另一个高频翻车点是建边方向:prerequisites[i] = [b, a] 表示先修 a 才能修 b,要建 a → b、给 b 加入度。记口诀「先修指向后修」;方向建反,样例可能碰巧过,逻辑整个是错的。
代码关键行(Python)
def canFinish(n, prerequisites):
graph = [[] for _ in range(n)]
indeg = [0] * n
for b, a in prerequisites: # 先修 a 才能修 b
graph[a].append(b) # 边 a -> b
indeg[b] += 1
queue = [i for i in range(n) if indeg[i] == 0]
done = 0
while queue:
cur = queue.pop(0); done += 1
for nxt in graph[cur]:
indeg[nxt] -= 1
if indeg[nxt] == 0:
queue.append(nxt)
return done == n # 计数判环,不看队列空最后一行 `done == n` 是整题的判环开关:环上的课永远进不了队,done 会小于 n。建边那两行的方向(a→b、b 加入度)是第二个必须核对的点。
常见追问
要输出一个合法的学习顺序怎么办?
就是课程表 II(LC210):同一套 Kahn 流程,把每次出队的课依次记进结果数组;最后数组长度等于课数就返回它,否则说明有环,返回空数组。
用 DFS 也能判环吗?
能,用三色标记:未访问(白)、访问中(灰)、已完成(黑)。DFS 路上遇到灰色节点,说明走回了当前路径,即有环。注意普通 visited 布尔标记判不了有向环——「访问过」不等于「在当前路径上」。
把这道题彻底吃透
LC 207「课程表」有逐步交互动画和完整图文题解,配着本页的结论看,一遍就通。