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
}

参考

图的遍历框架

相关推荐
小马爱打代码25 分钟前
Spring Boot 3.4 :@Fallback 注解 - 让微服务容错更简单
spring boot·后端·微服务
旷世奇才李先生1 小时前
奇哥面试记:SpringBoot整合RabbitMQ与高级特性,一不小心吊打面试官
spring boot·面试·java-rabbitmq
mrsk1 小时前
🧙‍♂️ CSS中的结界术:BFC如何拯救你的布局混乱?
前端·css·面试
曾曜1 小时前
PostgreSQL逻辑复制的原理和实践
后端
豌豆花下猫1 小时前
Python 潮流周刊#110:JIT 编译器两年回顾,AI 智能体工具大爆发(摘要)
后端·python·ai
轻语呢喃1 小时前
JavaScript :事件循环机制的深度解析
javascript·后端
ezl1fe1 小时前
RAG 每日一技(四):让AI读懂你的话,初探RAG的“灵魂”——Embedding
后端
经典19921 小时前
spring boot 详解以及原理
java·spring boot·后端
Aurora_NeAr1 小时前
Apache Iceberg数据湖高级特性及性能调优
大数据·后端
程序员清风2 小时前
程序员要在你能挣钱的时候拼命存钱!
后端·面试·程序员