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
}

参考

图的遍历框架

相关推荐
考虑考虑11 分钟前
Springboot3.5.x版本actuator新属性
spring boot·后端·spring
风象南26 分钟前
SpringBoot离线应用的5种实现方式
java·spring boot·后端
miniwa1 小时前
Python编程精进:正则表达式
后端·python
喵个咪1 小时前
MQTT 协议下的Last Will and Testament(LWT,遗嘱消息)
后端·go
恸流失8 小时前
DJango项目
后端·python·django
Mr Aokey10 小时前
Spring MVC参数绑定终极手册:单&多参/对象/集合/JSON/文件上传精讲
java·后端·spring
地藏Kelvin11 小时前
Spring Ai 从Demo到搭建套壳项目(二)实现deepseek+MCP client让高德生成昆明游玩4天攻略
人工智能·spring boot·后端
菠萝0112 小时前
共识算法Raft系列(1)——什么是Raft?
c++·后端·算法·区块链·共识算法
长勺12 小时前
Spring中@Primary注解的作用与使用
java·后端·spring
小奏技术12 小时前
基于 Spring AI 和 MCP:用自然语言查询 RocketMQ 消息
后端·aigc·mcp