题目来源:LeetCode210:课程表 II
问题抽象: 设计算法求解课程安排问题的可行学习序列(拓扑排序),需满足以下核心需求:
-
依赖关系建模:
- 给定课程总数
numCourses
(节点数0
至numCourses-1
); - 先修条件
prerequisites
:边列表[a_i, b_i]
表示b_i → a_i
(b_i
是a_i
的前置课程); - 目标:生成满足所有依赖关系的课程学习顺序(拓扑序)。
- 给定课程总数
-
可行性判定:
- 若依赖图无环(存在拓扑序),输出任意有效序列;
- 若依赖图有环(无法完成所有课程),输出空数组。
-
核心算法策略:
- Kahn 算法(入度表):
- 统计每个节点入度(前置课程数量);
- 队列维护当前可学课程(入度为
0
); - 每学完一门课,更新其后续课程的入度(入度归零则入队)。
- DFS 逆序:
- 后序遍历节点,反转序列得拓扑序(需检测环)。
- Kahn 算法(入度表):
-
复杂度约束:
- 时间复杂度 O(V+E)(节点数
V
+ 边数E
); - 空间复杂度 O(V+E)(邻接表 + 入度表 + 队列)。
- 时间复杂度 O(V+E)(节点数
-
边界处理:
- 课程数
0
时返回空数组[]
; - 无先修条件时:返回任意顺序(如
[0,1,...,numCourses-1]
); - 环检测:如
[[1,0],[0,1]]
(两门课互斥),输出[]
; - 多解场景:输出任意有效解(如
[0,2,1,3]
或[0,1,2,3]
均可)。
- 课程数
输入:整数 numCourses
(1 ≤ numCourses ≤ 2000
);先修条件列表 prerequisites
(0 ≤ prerequisites.length ≤ 5000
,每项为长度 2
的列表)
输出:整数数组(拓扑序列,长度 =numCourses
;若不可行则返回空数组 []
)
解题思路
本题是一个典型的拓扑排序问题。拓扑排序用于解决有向无环图(DAG)中节点依赖关系的线性排序问题。具体思路如下:
-
构建图与入度统计:
- 使用邻接表表示课程依赖关系:
graph[i]
存储课程i
的所有后续课程。 - 统计每个课程的入度(即指向该课程的边数)。
- 使用邻接表表示课程依赖关系:
-
初始化队列:
- 将所有入度为 0 的课程加入队列(这些课程没有先修课,可立即学习)。
-
BFS 拓扑排序:
- 从队列中取出课程(入度为 0),加入结果数组。
- 遍历该课程的所有后续课程,将其入度减 1(相当于删除依赖关系)。
- 若后续课程的入度减至 0,则加入队列。
-
结果验证:
- 若结果数组长度等于课程总数,则返回该数组(所有课程可完成)。
- 否则返回空数组(存在循环依赖)。
亮点:
- 使用数组存储邻接表(避免
ArrayList
扩容开销)。 - 使用数组存储入度,提高访问效率。
- 使用队列进行 BFS,确保时间复杂度为 O(N + E)(N 为节点数,E 为边数)。
代码实现(Java版)
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 初始化邻接表(使用数组存储,每个元素是一个列表)
List<Integer>[] graph = new ArrayList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new ArrayList<>();
}
// 初始化入度数组
int[] inDegree = new int[numCourses];
// 构建图并统计入度
for (int[] edge : prerequisites) {
int a = edge[0]; // 目标课程
int b = edge[1]; // 先修课程
graph[b].add(a); // b -> a 的有向边
inDegree[a]++; // a 的入度加1
}
// 初始化队列(存储入度为0的节点)
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
// 结果数组及索引
int[] result = new int[numCourses];
int index = 0;
// BFS 拓扑排序
while (!queue.isEmpty()) {
int course = queue.poll();
result[index++] = course; // 将当前课程加入结果
// 遍历当前课程的所有后续课程
for (int next : graph[course]) {
inDegree[next]--; // 后续课程入度减1
if (inDegree[next] == 0) {
queue.offer(next); // 入度为0则加入队列
}
}
}
// 检查是否所有课程都被安排
if (index == numCourses) {
return result;
} else {
return new int[0]; // 存在环,返回空数组
}
}
}
代码说明
-
数据结构:
graph
:邻接表,存储每个课程指向的后续课程。inDegree
:数组,存储每个课程的入度(依赖的课程数量)。queue
:队列,用于 BFS 遍历入度为 0 的课程。result
:结果数组,存储拓扑排序序列。
-
关键步骤:
- 建图:遍历
prerequisites
,构建邻接表并更新入度数组。 - 初始化队列:将所有入度为 0 的课程加入队列。
- BFS 遍历:
- 取出队首课程加入结果数组。
- 将其后续课程的入度减 1,若入度变为 0 则入队。
- 验证结果:若结果数组长度等于课程总数,则返回;否则返回空数组。
- 建图:遍历