05 August 2008
There are a total of ‘numCourses’ courses you have to take, labeled from ‘0’ to ‘numCourses-1’.
Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: ‘[0,1]’
Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?
Example 1:
1
2
3
4
Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0. So it is possible.
Example 2:
1
2
3
4
5
Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0, and to take course 0 you should
also have finished course 1. So it is impossible.
Constraints:
这种有前置条件的题目要养成思维,这道题可以简化成: 课程安排图是否可以称为 有向无环图(Directed Acyclic Graph, DAG),即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。
思路:通过拓扑排序判断此课程安排图是否是 有向无环图(DAG) 。 拓扑排序原理: 对 DAG 的顶点进行排序,使得对每一条有向边 (u, v),均有 u(在排序记录中)比 v 先出现。亦可理解为对某点 v 而言,只有当 v 的所有源点均出现了,v 才能出现。
通过课程前置条件列表 ‘prerequisites’ 可以得到课程安排图的 邻接表 ‘adjacency’,以降低算法时间复杂度,以下两种方法都会用到邻接表。
方法1 - BFS (in-degree table)
时间复杂度 O(N + M): 遍历一个图需要访问所有节点和所有临边,N 和 M 分别为节点数量和临边数量;
空间复杂度 O(N + M): 为建立邻接表所需额外空间,adjacency 长度为 N ,并存储 MM条临边的数据。
用一个HashMap来记录依赖的出度课程,这个是背诵模板
1
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
34
35
36
37
38
39
40
41
42
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// HashMap来记录课程和该课程的出度课程的顶点
HashMap<Integer, HashSet<Integer>> map = new HashMap<>();
// 数组记录每个课程的入度
int[] inDegrees = new int[numCourses];
for (int[] pre : prerequisites) {
inDegrees[pre[0]]++;
// key - 课程
// value - 依赖该课程的别的课程
map.computeIfAbsent(pre[1], k -> new HashSet<>()).add(pre[0]);
}
Queue<Integer> queue = new ArrayDeque<>();
// 找到入度为0的课程,也就是不依赖别的课程的课程
for (int i = 0; i < numCourses; i++) {
if (inDegrees[i] == 0) {
queue.offer(i);
}
}
while (!queue.isEmpty()) {
int currCourse = queue.poll();
numCourses--;
// 独立的课程,虽然入度为0,但跟任何别的课程没有联系
// 即没通过prerequisites关系存入到map中的那些课程
if (!map.containsKey(currCourse)) {
continue;
}
for (int followingCourse : map.get(currCourse)) {
inDegrees[followingCourse]--; // 修掉key先修课程,依次给value课程减掉一次依赖
if (inDegrees[followingCourse] == 0) { // value课程没有先修课程了,否则,则需要先上另外的先修课程
queue.offer(followingCourse); // value课程可能是别的课程的先修课程
}
}
}
return numCourses == 0;
}
}
用邻接表
1
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
34
35
36
37
38
39
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses]; // 入度表
List<List<Integer>> adjacency = new ArrayList<>(); //邻接表存储图
//BFS
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
adjacency.add(new ArrayList<>());
}
// 给每门课添加入度,以及将入度信息和课程本身添加到邻接表中
for (int[] pre : prerequisites) {
indegrees[pre[0]]++;
adjacency.get(pre[1]).add(pre[0]);
}
// 将入度为0(没有前置课程的)的课程索引初始化入队
for (int i = 0; i < numCourses; i++) {
if (indegrees[i] == 0) {
queue.offer(i);
}
}
// BFS拓扑排序
while (!queue.isEmpty()) {
int pre = queue.poll();
numCourses--;
for (int cur : adjacency.get(pre)) { // 当前课程减去入度
indegrees[cur]--;
if (indegrees[cur] == 0) {
queue.offer(cur);
}
}
}
return numCourses == 0;
}
}
因为课程是数字来表示,所以可以不用邻接表来存储每个入度,直接相加即可
1
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
34
35
36
37
38
39
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] results = new int[numCourses];
int[] indegrees = new int[numCourses]; //入度表也可以用hashmap表示
// 创建入度表
for (int[] pre : prerequisites) {
indegrees[pre[0]]++; // 把每个入度简化为增加1
}
Queue<Integer> queue = new ArrayDeque<>();
// 首先将入度为0的顶点放进来
for (int i = 0; i < numCourses; i++) { // 课程表用数字表示
if (indegrees[i] == 0) {
queue.offer(i);
}
}
int count = 0; // 记录课程是否全部被修完
while (!queue.isEmpty()) {
int currCourse = queue.poll();
results[count] = currCourse; // 把访问到的顶点(修完的课)加入到结果中
count++;
// 更新剩余未访问的顶点(未修的课)的入度,并将入度为0的课加入到队列中准备访问
for (int[] pre : prerequisites) {
if (pre[1] == currCourse) { // 只用修改依赖课程是当前课程的课程
indegrees[pre[0]]--; // 依赖课程是当前课程的课程的入度--11
if (indegrees[pre[0]] == 0) { // 入度为0则将课程加入到BFS的下一层中
queue.offer(pre[0]);
}
}
}
}
// 检查下全部课程是否都被修完
return count == numCourses;
}
}
方法2 - DFS,通过 DFS 判断图中是否有环。
时间复杂度 O(N + M): 遍历一个图需要访问所有节点和所有临边,N和 M分别为节点数量和临边数量;
空间复杂度 O(N + M): 为建立邻接表所需额外空间,adjacency 长度为 N ,并存储 M 条临边的数据。
1
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
34
35
36
37
38
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> adjacency = new ArrayList<>();// 邻接表存储图
for (int i = 0; i < numCourses; i++) {
adjacency.add(new ArrayList<>());
}
int[] flags = new int[numCourses];
for (int[] pre : prerequisites) {
adjacency.get(pre[1]).add(pre[0]);
}
for (int i = 0; i < numCourses; i++) {
if (!dfs(adjacency, flags, i)) {
return false;
}
}
return true;
}
private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {
if (flags[i] == 1) {
return false;
}
if (flags[i] == -1) {
return true;
}
flags[i] = 1;
for (int j : adjacency.get(i)) {
if (!dfs(adjacency, flags, j)) {
return false;
}
}
flags[i] = -1;
return true;
}
}