# 207. 课程表
# 题目描述
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。
在选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a, b],表示如果要学习课程 a,必须先完成课程 b。
请判断是否可能完成所有课程。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:先学习课程 0,再学习课程 1。
2
3
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:两门课程互相依赖,形成了环。
2
3
提示:
1 <= numCourses <= 20000 <= prerequisites.length <= 5000prerequisites[i].length == 2- 所有课程对互不相同
# 思路
先把课程关系翻译成图。
[a, b] 表示学 a 之前必须学 b,所以应该连一条 b -> a 的有向边。这里千万别把方向写反了。
什么时候所有课程无法完成?课程依赖图中出现环的时候。
例如 0 -> 1 -> 0,想学 0 要先学 1,想学 1 又要先学 0,谁也不能成为第一门课。
拓扑排序不是普通意义上的数值排序,而是把一个有向无环图转换成线性顺序。实现拓扑排序可以使用 Kahn 算法(BFS),也可以使用 DFS,本题掌握更直观的 BFS 即可。
以代码随想录图论章节中的依赖图为例:
为什么肉眼能找到节点 0 作为开头?因为它的入度为 0,没有任何节点指向它。只有这样的节点,才不需要等待其他前置任务。
本题用 Kahn 算法做拓扑排序:
- 统计每门课程的入度,入度表示还有多少门先修课没有完成;
- 把所有入度为 0 的课程加入队列,它们现在就可以学习;
- 每学完一门课,就删除它指向的边,让后续课程的入度减一;
- 某门后续课程的入度变为 0 时,加入队列;
- 最后比较学完的课程数和
numCourses。
为什么剩下的课程就是环?如果队列已经空了,却还有课程没学,说明剩余每个节点的入度都大于 0。它们互相等待,不可能再找到一个合法起点。
拓扑排序的本质,就是反复找到入度为 0 的节点,将它加入结果集,再将它从图中移除。 代码中不需要真的删除节点,只要把它指向节点的入度减一即可。
# 模拟过程
先找到入度为 0 的节点 0,将它加入结果集:
移除节点 0,也就是将它指向节点的入度减一:
此时节点 1 和节点 2 的入度都变为 0,选哪一个都可以,所以拓扑排序结果可能不唯一。这里先选择节点 1:
移除节点 1 后继续寻找入度为 0 的节点:
不断重复“选入度为 0 的节点、将其指向节点的入度减一”,就能得到一条合法顺序。
如果图中存在有向环,会发生什么?
节点 0 被处理之后,剩余节点形成环,再也找不到入度为 0 的节点。此时处理过的节点数小于课程总数,就可以判断无法完成全部课程。
# 解题代码
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses);
vector<int> inDegree(numCourses, 0);
for (const auto& edge : prerequisites) {
int course = edge[0];
int prerequisite = edge[1];
graph[prerequisite].push_back(course); // 先修课指向后续课
++inDegree[course];
}
queue<int> courses;
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) courses.push(i);
}
int learned = 0;
while (!courses.empty()) {
int current = courses.front();
courses.pop();
++learned;
for (int next : graph[current]) {
--inDegree[next]; // current 学完,移除一条前置依赖
if (inDegree[next] == 0) courses.push(next);
}
}
return learned == numCourses;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 复杂度分析
- 时间复杂度:O(V + E),V 是课程数,E 是依赖关系数。
- 空间复杂度:O(V + E),邻接表、入度数组和队列所需空间。
# 其他语言
# Python3
from collections import deque
class Solution:
def canFinish(self, numCourses, prerequisites):
graph = [[] for _ in range(numCourses)]
in_degree = [0] * numCourses
for course, prerequisite in prerequisites:
graph[prerequisite].append(course)
in_degree[course] += 1
queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
learned = 0
while queue:
current = queue.popleft()
learned += 1
for next_course in graph[current]:
in_degree[next_course] -= 1
if in_degree[next_course] == 0:
queue.append(next_course)
return learned == numCourses
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Java
class Solution {
public boolean canFinish(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 i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) queue.offer(i);
}
int learned = 0;
while (!queue.isEmpty()) {
int current = queue.poll();
learned++;
for (int next : graph.get(current)) {
if (--inDegree[next] == 0) queue.offer(next);
}
}
return learned == numCourses;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Go
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := make([][]int, numCourses)
inDegree := make([]int, numCourses)
for _, edge := range prerequisites {
course, prerequisite := edge[0], edge[1]
graph[prerequisite] = append(graph[prerequisite], course)
inDegree[course]++
}
queue := []int{}
for i, degree := range inDegree {
if degree == 0 { queue = append(queue, i) }
}
learned := 0
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
learned++
for _, next := range graph[current] {
inDegree[next]--
if inDegree[next] == 0 { queue = append(queue, next) }
}
}
return learned == numCourses
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# JS
var canFinish = function(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const inDegree = new Array(numCourses).fill(0);
for (const [course, prerequisite] of prerequisites) {
graph[prerequisite].push(course);
inDegree[course]++;
}
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
let learned = 0, front = 0;
while (front < queue.length) {
const current = queue[front++];
learned++;
for (const next of graph[current]) {
inDegree[next]--;
if (inDegree[next] === 0) queue.push(next);
}
}
return learned === numCourses;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 与代码随想录联系
代码随想录的207.课程表讲解了同一套拓扑排序思路,图论主线中的拓扑排序则使用 ACM 输入输出模式完整实现。
做完本题可以继续做210.课程表 II。本题只问能不能完成,210 题还要返回一种合法的学习顺序,队列中节点的出队顺序正好就是答案。
评论
验证登录状态...