LeetCode207 课程表(带扩展)

leetcode.cn/problems/co...

解法一:有向图上的环检测

题意其实就是要求判断课程学习顺序之间是否存在循环依赖

看到依赖问题,首先想到的就是把问题转化成「有向图」这种数据结构,只要图中存在环,那就说明存在循环依赖

具体来说,我们首先可以把课程看成「有向图」中的节点,节点编号分别是 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
}

参考

图的遍历框架

相关推荐
得物技术8 分钟前
破解gh-ost变更导致MySQL表膨胀之谜|得物技术
数据库·后端·mysql
甜瓜看代码11 分钟前
Android事件分发机制
面试
小码编匠28 分钟前
WPF 中的高级交互通过右键拖动实现图像灵活缩放
后端·c#·.net
李重楼38 分钟前
前端性能优化之 HTTP/2 多路复用
前端·面试
Java水解41 分钟前
【MySQL】从零开始学习MySQL:基础与安装指南
后端·mysql
Java水解43 分钟前
Scala深入面向对象:类、对象与伴生关系
后端·scala
LeeGe44 分钟前
基于binlog的canal到redis的同步
后端
kfyty7251 小时前
loveqq-bootstrap 和 springcloud-bootstrap 有什么区别
后端·架构
RoyLin1 小时前
TypeScript设计模式:桥接模式
前端·后端·typescript
CryptoRzz1 小时前
印度尼西亚股票数据API对接实现
javascript·后端