← 返回题目列表

课程安排问题如何用拓扑排序判断能否完成所有课程?

高频 中等 第 3 / 30 题 更新于 2026/07/30
拓扑排序Kahn算法有向图环检测

简化版

课程安排是依赖关系图:b -> a 表示学 a 之前必须先学 b。如果这张有向图有环,就不可能完成所有课程;如果能拓扑排序处理完所有节点,就可以完成。

详细版

常用 Kahn 算法:先统计每门课的入度,入度为 0 的课表示当前没有前置依赖,可以入队。每学完一门课,就把它指向的后续课程入度减 1;如果某个后续课程入度变成 0,就入队。最后统计处理过的课程数,如果等于 numCourses,说明无环,可以完成;否则有环。

DFS 也能做,用三色标记:0 未访问、1 当前递归栈中、2 已完成。DFS 时遇到状态 1 的节点说明有环。Kahn 更适合输出学习顺序,DFS 更适合递归判环。

完整版教学

一、如何把课程依赖建成有向图

题目给的先修关系通常是 [a, b],含义是学课程 a 前必须先学 b。因此边应该从 b 指向 a,表示“完成 b 后,a 的一个前置条件被满足”。边方向写反,拓扑结果会反过来,甚至影响你对入度的理解。

prerequisites = [[1,0], [2,0], [3,1], [3,2]]

0 -> 1 -> 3
 \        ^
  -> 2 ---|

这张图里 0 没有先修课,先学 0,然后 1 和 2 都可学,最后学 3。它能完成,因为不存在循环依赖。

二、入度表示还欠多少前置课程

入度是理解 Kahn 算法的核心。对课程 x 来说,indegree[x] 表示还有多少门前置课程没完成。入度为 0 的节点就是当前可以学习的课程。

节点初始入度含义
00无前置课程,可以先学
11还需要 0
21还需要 0
32还需要 1 和 2

当学完 0 后,1 和 2 的入度各减 1,变为 0,可以入队。等 1、2 都完成后,3 的入度从 2 减到 0,才能学习。这个过程本质是在不断删除图中的入度为 0 的节点。

三、Kahn 算法的完整流程

Kahn 算法使用队列保存当前入度为 0 的节点。每弹出一个节点,就认为它已经被安排进学习顺序,然后遍历它指向的邻居,把邻居入度减 1。

boolean canFinish(int n, int[][] prerequisites) {
    List<Integer>[] graph = new ArrayList[n];
    for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
    int[] indeg = new int[n];
    for (int[] p : prerequisites) {
        int course = p[0], pre = p[1];
        graph[pre].add(course);
        indeg[course]++;
    }
    Queue<Integer> q = new ArrayDeque<>();
    for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
    int learned = 0;
    while (!q.isEmpty()) {
        int cur = q.poll();
        learned++;
        for (int next : graph[cur]) {
            if (--indeg[next] == 0) q.offer(next);
        }
    }
    return learned == n;
}

如果最后 learned < n,说明剩下的节点互相依赖,谁都等不到入度变成 0,这就是有向环。

四、为什么有环就无法完成

有向环表示一组课程互相等待。例如 0 -> 1 -> 2 -> 0,要学 1 先学 0,要学 2 先学 1,要学 0 又先学 2。这个循环没有起点,队列里不会出现这几个节点。

0 -> 1 -> 2
^         |
|---------|

每个节点入度都是 1
没有入度为 0 的入口

如果整张图只有这 3 门课,初始队列为空,处理数量是 0,不等于 3,返回 false。如果图里还有其他无环部分,Kahn 会处理那些部分,但环上的节点仍然处理不到。

五、DFS 三色法怎么判环

DFS 判环用状态数组:0 未访问,1 正在访问,2 已完成。如果从当前递归路径再次遇到状态 1 的节点,就说明存在回边,也就是有环。

visit(u):
  if state[u] == 1: return false
  if state[u] == 2: return true
  state[u] = 1
  for v in graph[u]:
    if !visit(v): return false
  state[u] = 2
  return true

Kahn 是从“依赖入口”消除节点,DFS 是从“递归路径”发现回边。两个方法都常考,能同时讲出来会显得理解更完整。

六、复杂度与边界条件

建图扫描 E 条先修关系,拓扑遍历访问 V 个课程和 E 条边,所以时间复杂度是 O(V + E)。邻接表、入度数组和队列占用 O(V + E) 空间。

边界要注意:没有先修关系时,所有课程入度为 0,返回 true;存在重复边时,如果题目不保证唯一,入度会被重复增加,需要按输入语义处理,通常平台默认先修对唯一;课程编号范围是 0..n-1,数组大小按 n 开。

七、常见误区与追问

记忆钩子:课程安排就是“依赖图能不能被剥洋葱式剥空”;剥不空,剩下的就是环。

  • 误区:把边建成 a -> b [a,b] 表示 b 是 a 的先修课,通常应建 b -> a,这样入度才表示还欠的前置课程数。
  • 误区:队列初始只放一门课。 所有入度为 0 的课程都要入队,因为它们都可以作为起点。
  • 误区:处理到队列为空就直接返回 true。 必须比较处理数量是否等于总课程数,队列为空可能是因为剩下节点成环。
  • 追问:如何返回可行课程顺序? Kahn 弹出节点的顺序就是一种拓扑序;如果最终数量不足,就返回空。
  • 追问:DFS 里状态 1 和状态 2 有什么区别? 状态 1 表示还在当前递归栈中,遇到它是环;状态 2 表示已经确认无环,可以复用结果。
  • 追问:多个合法顺序怎么办? 拓扑序不唯一,队列里多个入度为 0 节点时任选一个都可以。

八、加强记忆

课程安排的核心是有向图判环。Kahn 算法把入度当成“还欠多少先修课”,从所有入度为 0 的课程开始学,学完一门就释放它的后继课程;能处理完全部课程就是无环。DFS 三色法从递归路径角度判环,遇到正在访问的节点就是循环依赖。