LeetCode LCR 113. 课程表 II
题目描述


题意分析
prerequisites中的[a, b]表示学习a前必须先完成b。返回包含全部课程的一种合法学习顺序,答案不唯一;若依赖关系使部分课程无法完成,则返回空数组。
解法:入度归零的拓扑排序
核心思路
[!blue]
将先修关系建成有向边
b -> a,邻接表保存每门课完成后能影响哪些后续课程。indegree[x]表示课程x还有多少条先修关系未解除;它为 0 时,这门课才可以学习。先将所有零入度课程入队,包括没有依赖的孤立课程。每次取出一门课写入
order,再沿其出边将后继入度减一;只有刚好减到 0 的后继才入队。这使已写入结果的每门课,其先修课都排在它前面,也避免每轮重新检查全部课程。若队列为空时仍有课程没输出,这些剩余节点都还有来自剩余节点的入边。不断沿未完成的先修关系向前追溯,在有限节点中必然遇到重复节点,因此剩余图中存在环。未输出的课程可能位于环中,也可能依赖这个环,都不能纳入完整学习顺序。
所以输出数量等于
numCourses时,order就是合法答案;数量不足时返回空数组。多个零入度课程可以任选一个先处理,本题不要求唯一顺序或最小字典序。
解题步骤
- 为每门课程建立邻接表;每条
[a, b]加入边b -> a,并增加a的入度。- 扫描全部课程,将入度为 0 的课程加入队列。
- 取出队首课程,追加到结果中。
- 将它的每个后继入度减一,减到 0 时入队。
- 队列耗尽后检查结果长度:包含全部课程则返回结果,否则返回空数组。
代码实现
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
int[] indegree = new int[numCourses];
for (int[] edge : prerequisites) {
graph.get(edge[1]).add(edge[0]);
indegree[edge[0]]++;
}
Queue<Integer> queue = new ArrayDeque<>();
for (int course = 0; course < numCourses; course++) {
if (indegree[course] == 0) {
queue.offer(course);
}
}
int[] order = new int[numCourses];
int idx = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
order[idx++] = course;
// 学完当前课程后,释放所有依赖它的后续课程。
for (int next : graph.get(course)) {
indegree[next]--;
if (indegree[next] == 0) {
queue.offer(next);
}
}
}
return idx == numCourses ? order : new int[0];
}
}
func findOrder(numCourses int, prerequisites [][]int) []int {
graph := make([][]int, numCourses)
indegree := make([]int, numCourses)
for _, edge := range prerequisites {
course := edge[0]
pre := edge[1]
graph[pre] = append(graph[pre], course)
indegree[course]++
}
queue := make([]int, 0)
for course := 0; course < numCourses; course++ {
if indegree[course] == 0 {
queue = append(queue, course)
}
}
order := make([]int, 0, numCourses)
for head := 0; head < len(queue); head++ {
course := queue[head]
order = append(order, course)
// 入度降为 0 时,说明该课程的先修课已经全部完成。
for _, next := range graph[course] {
indegree[next]--
if indegree[next] == 0 {
queue = append(queue, next)
}
}
}
if len(order) != numCourses {
return []int{}
}
return order
}
复杂度分析
- 时间复杂度:$O(n+m)$,
n为课程数,m为先修关系数。每门课至多入队、出队一次,每条边只在起点出队时处理一次。- 空间复杂度:$O(n+m)$,邻接表保存全部边,入度数组、队列和结果各占 $O(n)$。
关键点总结
[!green]
- 边从先修课指向后续课,入度反映尚未满足的依赖。
- 初始加入全部零入度课程,才能覆盖所有互不相连的部分。
- 出队时记录学习顺序,最后用输出数量判断是否存在无法完成的课程。
- 队列中同时存在多个选择不代表无解,任意选择都能继续得到合法拓扑顺序。
易错点总结
[!yellow]
- 先修课指向后续课,入度表示仍未完成的先修数量。
- 所有入度 0 的课程都先入队,后继只在减到 0 时加入。
- 输出数量不足课程总数就返回空数组,任意合法拓扑顺序都可接受。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 207. 课程表 | 中等 | 依赖图相同,原题只判断能否完成,本题还需输出一个合法拓扑顺序。 |
| 802. 找到最终的安全状态 | 中等 | 同样可用度数逐步剥离节点,原题在反图中从终点消除安全节点。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!