LeetCode 207. 课程表
问题描述
给定课程总数 numCourses
和课程依赖关系 prerequisites
,判断是否能完成所有课程。依赖关系 prerequisites[i] = [a_i, b_i]
表示学习课程 a_i
前必须先完成课程 b_i
。
示例:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:需要先完成课程 0 才能学习课程 1,可以完成。
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:课程 0 和课程 1 互相依赖,形成环,无法完成。
算法思路:拓扑排序(Kahn算法)
- 构建有向图:使用邻接表表示课程依赖关系。
- 统计入度:记录每门课程的前置课程数量。
- 初始化队列:将所有入度为 0 的课程加入队列。
- BFS 处理:
- 从队列中取出课程,计数加 1。
- 将其后继课程的入度减 1。
- 若后继课程入度为 0,加入队列。
- 检测环:若完成课程数 ≠ 总课程数,说明存在环。
代码实现(拓扑排序)
import java.util.*;
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 1. 构建邻接表表示图
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
// 2. 入度数组初始化
int[] inDegree = new int[numCourses];
// 3. 遍历依赖关系,填充邻接表和入度数组
for (int[] edge : prerequisites) {
int from = edge[1]; // 起点课程(前置课程)
int to = edge[0]; // 终点课程(当前课程)
graph.get(from).add(to); // 添加边:from -> to
inDegree[to]++; // 终点课程入度增加
}
// 4. 初始化队列,将所有入度为0的课程加入队列
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
// 5. BFS遍历:记录已完成的课程数
int completedCourses = 0;
while (!queue.isEmpty()) {
int current = queue.poll(); // 取出当前课程
completedCourses++; // 完成课程计数
// 遍历当前课程的所有后继课程
for (int next : graph.get(current)) {
inDegree[next]--; // 后继课程入度减1
// 若后继课程入度为0,加入队列
if (inDegree[next] == 0) {
queue.offer(next);
}
}
}
// 6. 判断是否所有课程都完成
return completedCourses == numCourses;
}
}
代码注释
代码 | 说明 |
---|---|
List<List<Integer>> graph | 邻接表存储图结构:索引表示课程,列表存储该课程的后继课程 |
int[] inDegree | 入度数组:inDegree[i] 表示课程 i 的前置课程数量 |
graph.get(from).add(to) | 添加依赖关系边:from → to (需先完成 from 才能学 to ) |
inDegree[to]++ | 课程 to 的入度增加(因为新增一个前置依赖) |
queue.offer(i) | 入度为 0 的课程加入队列(无前置依赖,可直接学习) |
inDegree[next]-- | 学完当前课程后,后继课程 next 的依赖减少 |
completedCourses == numCourses | 判断是否所有课程都被处理(相等则无环) |
算法分析
- 时间复杂度:O(n + m)
- n 为课程数(构建邻接表)
- m 为依赖关系数(遍历边)
- 空间复杂度:O(n + m)
- 邻接表存储所有边(O(m))
- 入度数组和队列(O(n))
执行过程
输入:numCourses = 4
, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
- 构建图:
- 0 → [1, 2]
- 1 → [3]
- 2 → [3]
- 3 → []
- 入度数组:
[0, 1, 1, 2]
- 初始化队列:课程 0 入队(入度=0)
- BFS:
- 处理课程 0:计数=1,后继课程 1 和 2 入度减为 0 后入队
- 处理课程 1:计数=2,后继课程 3 入度减为 1
- 处理课程 2:计数=3,后继课程 3 入度减为 0 后入队
- 处理课程 3:计数=4
- 结果:完成课程数=4,返回
true
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 无依赖关系
System.out.println(solution.canFinish(3, new int[][]{})); // true
// 有环情况
System.out.println(solution.canFinish(2, new int[][]{{1,0},{0,1}})); // false
// 复杂依赖无环
int[][] deps = {{1,0},{2,1},{3,2}};
System.out.println(solution.canFinish(4, deps)); // true
// 复杂依赖有环
int[][] depsCycle = {{1,0},{2,1},{0,2}};
System.out.println(solution.canFinish(3, depsCycle)); // false
// 多分支有环
int[][] multiBranch = {{1,0},{2,0},{3,1},{1,3}};
System.out.println(solution.canFinish(4, multiBranch)); // false
}
关键点
- 拓扑排序核心:通过入度管理课程依赖,逐步解锁可学习课程。
- 环检测机制:若存在环,则环内课程的入度永不为 0,无法加入队列。
- 边界处理:
- 无依赖关系:所有课程入度=0,直接完成。
- 单课程:无需处理,直接返回
true
。
- 实际应用:课程安排、任务调度、编译顺序等依赖关系分析场景。
提示:若问题扩展为「输出学习顺序」,只需在 BFS 过程中记录出队顺序即可得到拓扑序列。