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
}

参考

图的遍历框架

相关推荐
上进小菜猪9 小时前
把 KES 集群交给 Kubernetes 的九十天:一个 DBA 的试用实录
后端
两点王爷9 小时前
PostgreSQL 常用 SQL 语句与 GIS 相关函数详解
数据库·后端
考虑考虑10 小时前
Springboot环境变量占位符语法
spring boot·后端·spring
步行cgn12 小时前
Spring 基于 XML 的自动装配:byName 详解
java·后端·spring
Alice-YUE12 小时前
工业级 AI Agent 架构:5 层、2 范式、4 原则,一次讲透
面试·系统架构·大模型·ai agent·智能体
RISCV_Explorer13 小时前
RISC-V启动与运行时规范机制解析——BRS约定、HSM多核启动与设备树要求
后端·risc-v
星栈独行13 小时前
ADK-Rust 是什么?Rust 生态新一代 AI Agent 开发套件
人工智能·后端·rust
JavaGuide14 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
用户83562907805114 小时前
使用 Python 将 DOCX 转换为 Markdown
后端·python
IT_陈寒14 小时前
Vue computed属性这个坑,我居然踩了三次才爬出来
前端·人工智能·后端