题目描述

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai必须 先学习课程 bi

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

示例 1:

1
2
3
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

1
2
3
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

  • 1 <= numCourses <= 10^5
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

解法

  • 深度优先搜索+拓扑排序
    • 对于图中的任意一个节点,它在搜索的过程中有三种状态,即:
      • 「未搜索」:我们还没有搜索到这个节点;
      • 「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成);
      • 「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。
    • 我们将当前搜索的节点u标记为「搜索中」,遍历该节点的每一个相邻节点 v:
      • 如果 v 为「未搜索」,那么我们开始搜索 v,待搜索完成回溯到 u;
      • 如果 v 为「搜索中」,那么我们就找到了图中的一个环,因此是不存在拓扑排序的;
      • 如果 v 为「已完成」,那么说明 v 已经在栈中了,而 u 还不在栈中,因此 u无论何时入栈都不会影响到 (u, v) 之前的拓扑关系,以及不用进行任何操作。
    • 当 u的所有相邻节点都为「已完成」时,我们将 u 放入栈中,并将其标记为「已完成」。
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
43
44
45
46
47
class Solution {
List<List<Integer>> edges;
boolean valid = true;
int[] visited;
public boolean canFinish(int numCourses, int[][] prerequisites) {
//初始化
edges = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];

//先学[1] 后学[0],组建映射关系
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}

//有多少门课就有多少入口进行深度优先搜索
for (int i = 0; i < numCourses && valid; i++) {
if (visited[i] == 0) {
dfs(i);
}
}

return valid;
}

private void dfs(int i) {
//1.标记当前状态
visited[i] = 1;
for (int num : edges.get(i)) {
if (visited[num] == 0) {
//未访问,进行dfs
dfs(num);
if (!valid) {
//出现环,停止搜索
return;
}
} else if (visited[num] == 1) {
//搜索中 找到了环,确定结果,返回
valid = false;
return;
}
}
visited[i] = 2;
}
}
  • 时间复杂度: O(n+m),其中 n 为课程数,m 为先修课程的要求数。这其实就是对图进行深度优先搜索的时间复杂度。

  • BFS

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
class Solution {
/*
拓扑排序解决
1.将入度为0的节点加入队列
2.出队列的时候将这个顶点的所有临界点的入度减1,并将出队列的元素加入结果集合,如果入度为0则继续入队列
3.最后检查结果集合中的元素是否和课程数相同
*/
public boolean canFinish(int numCourses, int[][] prerequisites) {
//存储各个课程的入度
int[] input = new int[numCourses];
//入度为0的顶点个数
int count = 0;
LinkedList<Integer> queue = new LinkedList<>();
for(int[] pre:prerequisites){
//统计入度
input[pre[0]]++;
}
//寻找入度为0的元素,入队
for(int i=0;i<numCourses;i++){
if(input[i]==0){
queue.addLast(i);
}
}

while(!queue.isEmpty()){
int node = queue.removeFirst();
count++;
//把与node相关的临界点的入度减1
for(int[] pre:prerequisites){
if(pre[1]==node){
input[pre[0]]--;
if(input[pre[0]]==0){
queue.addLast(pre[0]);
}
}
}
}

return count==numCourses;
}
}

来源:力扣(LeetCode)
链接:207. 课程表 - 力扣(LeetCode)