问题
There are a total of n courses you have to take, labeled from 0 to n-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:
Input: 2, [[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:
Input: 2, [[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.
Note:
- The input prerequisites is a graph represented by a list of edges, not adjacency matrices. Read more about how a graph is represented.
- You may assume that there are no duplicate edges in the input prerequisites.
思路与解法
这道题目的要求是让我们判断是否可以找到一个课程的顺序,满足所有先修课程都在后续课程之前学习。
方法一:
首先,最容易想到的方法便是找到一个所有课程的拓扑序列,如果找不到,则说明输入数据并不满足题目要求,return false即可。但是该题目并不需要找到完成的拓扑序列,我们只需要判断输入的“图”是否存在环即可。此时,我们可以利用DFS递归实现:
代码实现
此算法我用go语言实现:
- 对于无向图,判断是否有环,我们可以从一个节点出发,并以后继节点不断递归,对访问过的点进行标记,如果找到与一个已经标记过的点,则表明该图中存在环。如果不是连通图,则可以在最外层嵌套一层循环,以每个未访问过的节点为起点进行递归搜索。
- 对于有向图,不管是否连通,我们都应该在最外层嵌套一层循环,以每个未访问的节点为起点进行递归搜索:因为有向边限制了从一个节点出发并不一定能够遍历整个图。另一方面,在有向图的遍历中,每个节点有三个状态(无向图只需要两个):未访问的节点,标记为
0
;在一次递归过程中已访问的节点,标记为-1
;完全访问过的节点标记为1
。
之所以存在状态-1
,也是因为有向边限制从一个节点出发并不一定能够遍历整个图。所以,在之后再次遍历该有向图的时候,可以不受之前是否访问过的干扰;状态1则表明了某个节点已经访问过,减少最外层循环的次数。
具体实现如下:
// 递归判断是否存在环
func findCycle(s int, visited []int, graph map[int][]int) bool {
visited[s] = -1 // 将再一次递归过程中访问过的点标记为-1
for _, edge := range graph[s] { // 遍历其后继节点
if visited[edge] == -1 { // 存在环
return true
} else if visited[edge] == 0 { // 从为访问过的点出发继续递归
if findCycle(edge, visited, graph) {
return true
}
}
}
visited[s] = 1 // 在回溯过程中,将此次递归中的所有访问过的节点标记为1,因为这些节点并不构成环,所以外层循环再次遍历这些节点并没有意义
return false
}
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := make(map[int][]int)
visited := make([]int, numCourses)
// 构造图,pair[0] -> pair[1]存在一条边,表示pair[1]必须在pair[0]之前修习
for _, pair := range prerequisites {
graph[pair[1]] = append(graph[pair[1]], pair[0])
}
for i:=0; i<numCourses; i++ {
if visited[i] == 0 {
if findCycle(i, visited, graph) {
return false
}
}
}
return true
}
遇到的问题
最初findCycle
写法如下:
func findCycle(s int, visited []int, graph map[int][]int) bool {
visited[s] = -1 // 将再一次递归过程中访问过的点标记为-1
for _, edge := range graph[s] { // 遍历其后继节点
if visited[edge] == -1 { // 存在环
return true
} else if visited[edge] == 0 { // 从为访问过的点出发继续递归
return findCycle(edge, visited, graph)
}
}
visited[s] = 1 // 在回溯过程中,将此次递归中的所有访问过的节点标记为1,因为这些节点并不构成环,所以外层循环再次遍历这些节点并没有意义
return false
}
由于对后续节点的判断中,return findCycle(edge, visited, graph)
,导致回溯过程中visited[s]无法置为1,所以结果一直不对。
方法二:
我们可以采用剥洋葱的思想,将先修课程修完,然后从后续的课程中,寻找“先修课程”,直到所有的课程都修完一遍时,return true
;否则,如果有某些课程始终学习不到,return fasle
。
代码实现
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := make(map[int][]int)
in_degree := make([]int, numCourses)
queue := make([]int, 0)
// count用来计数修习过的课程
count := 0
// 构造图,pair[0] -> pair[1]存在一条边,表示pair[1]必须在pair[0]之前修习
for _, pair := range prerequisites {
graph[pair[1]] = append(graph[pair[1]], pair[0])
in_degree[pair[0]]++ //统计pair[0]的入度
}
// 将所有入度为0的点加入到队列中
for i:=0;i<numCourses;i++ {
if in_degree[i] == 0 {
queue = append(queue, i)
count++
}
}
// 当queue为0时结束循环
for len(queue)!=0 {
head := queue[0]
queue = queue[1:]
// 遍历head的后继节点,并将其入度减少1;若入度为0,则加入到队列,count加1
for _, edge := range graph[head] {
in_degree[edge]--
if in_degree[edge] == 0 {
queue = append(queue, edge)
count++
}
}
}
// 若count==numCourses,则表明所有课程已经修习完毕
return count==numCourses
}
如果想要获得拓扑排列,则可以将上述代码中count
变量更改为schedule []int
,并在count++
的位置将节点添加到此切片中。
两种方法在时间复杂度上并没有什么不同,但是我更推荐第二种写法, 更易实现。