
解法一:有向图上的环检测
题意其实就是要求判断课程学习顺序之间是否存在循环依赖
看到依赖问题,首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖。
具体来说,我们首先可以把课程看成「有向图」中的节点,节点编号分别是 0, 1, ..., numCourses-1,把课程之间的依赖关系看做节点之间的有向边。比如说,要求必须修完课程 1 才能去修课程 3,那么,就有一条有向边从节点 1 指向节点 3。
如果发现这幅有向图中存在环,那就说明课程之间存在循环依赖,肯定没办法全部上完;反之,如果没有环,那么肯定能上完全部课程。
图的存储形式主要有邻接矩阵和邻接表,对于一幅有 V个节点,E条边的图,二者的空间复杂度分别是:
- 邻接矩阵:O(V^2)
- 邻接表:O(V+E)
所以,如果一幅图的边数远小于V^2(稀疏图),那么邻接表会更节省空间,反之如果边数很接近V^2,那实际上二者差不多。
分析该题意可发现,并不是所有课程之间都有依赖关系,也就是说图中并不是所有节点都相连,这是一张稀疏图,我们采用邻接表进行存储。
那么如何找环呢,其实就是要去遍历这张图,如果过程中遇到重复的节点,那不就说明成环了。遍历图有DFS和BFS两种方式。
DFS遍历
go
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := buildGraph(numCourses, prerequisites) // 构建一张有向图
onPath := make([]bool, numCourses) // 记录每次递归遍历路径上的节点
visited := make([]bool, numCourses) // 记录已经遍历过的节点,避免重复计算
var hasCycle bool
for i := 0; i < numCourses; i++ { // 由于图中并不是所有节点都相连,因此需要将每个节点都作为起点搜索一遍
traverse(graph, i, onPath, visited, &hasCycle)
}
return !hasCycle
}
func traverse(graph [][]int, nodeID int, onPath []bool, visited []bool, hasCycle *bool) {
if *hasCycle { // 已经找到了一个环即可确认答案
return
}
if onPath[nodeID] { // 该节点之前遍历过,再次相遇,说明成环了
*hasCycle = true
return
}
if visited[nodeID] { // 不重复遍历已遍历判断过的节点
return
}
// 标记当前节点已遍历过
onPath[nodeID] = true
visited[nodeID] = true
// DFS遍历所有相邻节点
for _, neighbor := range graph[nodeID] {
traverse(graph, neighbor, onPath, visited, hasCycle)
}
// 回溯,撤销选择
onPath[nodeID] = false
}
func buildGraph(numCourses int, prerequisites [][]int) [][]int {
graph := make([][]int, numCourses) // 每个课程即一个图上的节点
for idx := range graph {
graph[idx] = make([]int, 0)
}
// 根据课程依赖关系构建图上的边
for _, edge := range prerequisites {
from := edge[0]
to := edge[1]
graph[from] = append(graph[from], to)
}
return graph
}
BFS遍历
BFS一遍借助队列实现,这里为了判断是否有环,需要借助一些技巧,图有入度和出度的概念,如果一个节点 x有 a 条边指向别的节点,同时被 b条边所指向,则称节点 x的出度为 a,入度为 b。
go
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := buildGraph(numCourses, prerequisites)
inDegrees := make([]int, numCourses) // 记录每个节点的入度
for _, edge := range prerequisites {
to := edge[1]
inDegrees[to]++ // 被指向的节点入度加1
}
// 入度为0的才可以作为BFS遍历起点,即没有前置课程依赖可先开始学习,参考拓扑排序
queue := make([]int, 0)
for nodeID, inDegree := range inDegrees{
if inDegree == 0{
queue = append(queue, nodeID)
}
}
count := 0 // 记录遍历的总节点数
for len(queue) > 0{
size := len(queue)
for i:=0; i<size; i++{
// 弹出队头节点nodeID,遍历节点计数+1,并将其指向的节点入度都减1
nodeID := queue[0]
queue = queue[1:]
count++
for _, neighbor := range graph[nodeID]{
inDegrees[neighbor]--
if inDegrees[neighbor] == 0{ // 更新后入度为0,说明所有依赖neighbor的节点都已经遍历过了,即前置课程都已经修完了,那么当前课程也可以学习了
queue = append(queue, neighbor)
}
}
}
}
// 是否完成所有课程的学习
return count == numCourses
}
func buildGraph(numCourses int, prerequisites [][]int) [][]int{
graph := make([][]int, numCourses)
for idx := range graph{
graph[idx] = make([]int, 0)
}
for _, edge := range prerequisites{
from := edge[0]
to := edge[1]
graph[from] = append(graph[from], to)
}
return graph
}