拓扑排序 —— 2. 力扣刷题207. 课程表

news2025/4/15 0:36:12

在这里插入图片描述

题目链接:https://leetcode.cn/problems/course-schedule/description/
题目难度:中等
相关标签:拓扑排序 / 广度优先搜搜 BFS / 深度优先搜索 DFS

2.1 问题与分析

2.1.1 原题截图

在这里插入图片描述

2.1.2 题目分析

首先,理解题目后必须马上意识到考察的是 图论类型中的有向图问题,接着考虑到有向图之间的关系,可以想到我们在本系列第一篇 博客 提到的 拓扑排序

接着,这里将题目进一步简化:

给你 numCourses 门课程,和若干课程依赖关系 prerequisites,问你能否顺利修完所有课程。

换句话说:

  • 每门课程是一个节点。

  • 依赖关系是一个有向边(比如 [1, 0] 表示:想学 1,必须先学 0)。

这就构成了一个有向图,问题变成了:

这个图有没有环?

  • 有环 = 某些课程互相依赖,永远无法完成。

  • 无环 = 可以通过拓扑排序找到学习顺序。

在本系列的第一篇博客已经提到过,拓扑排序可以用来解决 判断有向图中是否有环问题

最后已经大概怎么知道解决问题了,那么请回答这个问题 如何通过拓扑排序解决有向图中是否有环问题

2.2 解法 1 —— 基于广度优先搜索(BFS)的拓扑排序(Kahn 算法)

不急着写代码,我们先用文字的形式,描述清楚解题思路。

2.2.1 解题思路

解题思路

  1. 构建图结构,使用二维数组存储,记作 graph
  2. 统计每个结点的入度,使用一维数组存储,记作 inCounts
  3. 维护一个队列 Q,将 inCounts 中入度为 0 的元素入队;
  4. 基于队列进行操作,将访问的结点存储到 results 数组中。
    a. 对于队中每个元素 e,将它的相邻结点的入度 减 1
    b. 如果 减 1 后的结点入度为 0,加入队列 Q
    c. 将 e 写入 results 数组。
    d. e 出列,循环此项操作。
  5. 判定 results 数组长度是否与所有元素数目相等。相等表示可以正常访问所有结点,原拓扑 无环,返回 true,否则返回 false。

复杂度分析

  • 时间复杂度: O(n+m),其中 n 为课程数,m 为先修课程的要求数。
  • 空间复杂度: O(n+m)。

2.2.2 代码实现

前面已经将解题方法写明白了,写代码就很方便了,我们分别使用 C++ / python 与 java 三种语言实现。

基于 C++ 的代码实现

实现思路请参考前文,先理解思路再看代码。

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> inCount(numCourses);
        vector<vector<int>> graph(numCourses);
        // [0, 1] 表示,需要先学习课程 1,才能学习课程 0
        // Step 1: 构建图
        // Step 2: 统计入度
        for (const auto& par: prerequisites) {
            graph[par[1]].push_back(par[0]);
            inCount[par[0]]++;
        }
        // Step 3: 初始化队列
        queue<int> Q;
        for (int i=0; i<numCourses; i++) {
            if (inCount[i] == 0) {
                Q.push(i);
            }
        }

		// Step 4:
        vector<int> results;
        while (!Q.empty()) {
            auto front = Q.front();
            Q.pop();
            results.push_back(front);
            for (auto v: graph[front]) {
                inCount[v]--;
                if (inCount[v] == 0) {
                    Q.push(v);
                }
            }
        }

        return results.size() == numCourses;
    }
};
基于 python 的代码实现

实现思路请参考前文,先理解思路再看代码。

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        inCount = [0] * numCourses
        graph = [[] for _ in range(numCourses)]
        # [0, 1] 表示,需要先学习课程 1,才能学习课程 0
        # Step 1: 构建图
        # Step 2: 统计入度
        for par in prerequisites:
            graph[par[1]].append(par[0])
            inCount[par[0]] += 1

        # Step 3: 初始化队列
        Q = deque()
        for i in range(numCourses):
            if inCount[i] == 0:
                Q.append(i)

        # Step 4:
        results = []
        while Q:
            front = Q.popleft()
            results.append(front)
            for v in graph[front]:
                inCount[v] -= 1
                if inCount[v] == 0:
                    Q.append(v)

        return len(results) == numCourses

基于 java 的代码实现

实现思路请参考前文,先理解思路再看代码。

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int[] inCount = new int[numCourses];
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }

        // [0, 1] 表示,需要先学习课程 1,才能学习课程 0
        // Step 1: 构建图
        // Step 2: 统计入度
        for (int[] par : prerequisites) {
            graph.get(par[1]).add(par[0]);
            inCount[par[0]]++;
        }

        // Step 3: 初始化队列
        Queue<Integer> Q = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inCount[i] == 0) {
                Q.offer(i);
            }
        }

        // Step 4:
        List<Integer> results = new ArrayList<>();
        while (!Q.isEmpty()) {
            int front = Q.poll();
            results.add(front);
            for (int v : graph.get(front)) {
                inCount[v]--;
                if (inCount[v] == 0) {
                    Q.offer(v);
                }
            }
        }

        return results.size() == numCourses;
    }
}

2.3 解法 2 —— 基于深度优先搜索(DFS)的拓扑排序

前面已经将解题方法写明白了,写代码就很方便了,我们分别使用 C++ / python 与 java 三种语言实现。

2.3.1 解题思路

解题思路

  1. 构建图,使用二维数组 graph 保存图的结构;
  2. 初始化过程:
    a. 初始化一维数组 visited,它的每个元素含有三个状态 0 表示未访问过,1 表示已经访问过,2 表示已经记录到最终结果数组中。注意这个地方必须保证更新顺序,也就是 0 -> 1 -> 2。
    b. 初始化 valid 变量,表示DFS过程中是否遇到 ,比如遇到环了就结束。
    c. 初始化 results 数组,用于记录已经访问过的路径。
  3. DFS 过程,该过程对每一个未访问过的结点进行考察,对于结点 e,完成操作包括:
    a. 更新 visited 数组,标记 e 结点已经被访问过。
    b. DFS 访问与 e 结点连接的其他结点。
    c. 如果下一个结点 visited 状态为 0, 则继续DFS;如果状态为 2,则说明有环,更新 valid 结束 DFS。
    d. 访问 e 结点的所有相邻结点后,将 e 记录到 results 中。
    e. 访问 e 结点以后,更新 visited[e] 的状态为 2,表示已经记录到 results 数组中。
  4. 判定 results 数组长度是否与所有元素数目相等。相等表示可以正常访问所有结点,原拓扑 无环,返回 true,否则返回 false。

2.3.2 代码实现

基于 C++ 的代码实现

实现思路请参考前文,先理解思路再看代码。

class Solution {
private:
    bool valid = true;
    vector<int> visited;
    vector<int> results;
    vector<vector<int>> graph;

    void dfs(int i) {
        visited[i] = 1;
        for (auto v: graph[i]) {
            if (visited[v] == 0) {
                dfs(v);
            } else if (visited[v] == 1) {
                valid = false;
                return;
            }
        }
        results.push_back(i);
        visited[i] = 2;
    }
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        graph = vector<vector<int>>(numCourses);
        for (const auto& par: prerequisites) {
            graph[par[1]].push_back(par[0]);
        }

        visited = vector<int>(numCourses, 0);
        for (int i=0; i<numCourses && valid; i++) {
            if (visited[i] == 0) {
                dfs(i);
            }
        }

        return results.size() == numCourses;
    }
};
基于 python 的代码实现

实现思路请参考前文,先理解思路再看代码。

class Solution:
    def __init__(self):
        self.valid = True
        self.visited = []
        self.results = []
        self.graph = []

    def dfs(self, i: int):
        self.visited[i] = 1
        for v in self.graph[i]:
            if self.visited[v] == 0:
                self.dfs(v)
                if not self.valid:
                    return
            elif self.visited[v] == 1:
                self.valid = False
                return
        self.results.append(i)
        self.visited[i] = 2

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        self.graph = [[] for _ in range(numCourses)]
        for par in prerequisites:
            self.graph[par[1]].append(par[0])

        self.visited = [0] * numCourses
        self.results = []
        self.valid = True

        for i in range(numCourses):
            if self.valid and self.visited[i] == 0:
                self.dfs(i)

        return len(self.results) == numCourses

基于 java 的代码实现

实现思路请参考前文,先理解思路再看代码。

class Solution {
    private boolean valid = true;
    private int[] visited;
    private List<Integer> results;
    private List<List<Integer>> graph;

    private void dfs(int i) {
        visited[i] = 1;
        for (int v : graph.get(i)) {
            if (visited[v] == 0) {
                dfs(v);
                if (!valid) return;
            } else if (visited[v] == 1) {
                valid = false;
                return;
            }
        }
        results.add(i);
        visited[i] = 2;
    }

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }

        for (int[] par : prerequisites) {
            graph.get(par[1]).add(par[0]);
        }

        visited = new int[numCourses];
        results = new ArrayList<>();
        valid = true;

        for (int i = 0; i < numCourses && valid; i++) {
            if (visited[i] == 0) {
                dfs(i);
            }
        }

        return results.size() == numCourses;
    }
}

2.4 总结

力扣的这道题可以作为 拓扑排序模板题,因为理解题目容易,必须建立在对 拓扑排序 的两种方法的了解的基础上,才能完成。中等难度,比较适合新手练习。

就题目而言,这道题本身就是判断是否有环的问题,通过拓扑排序实现而言。

继续强调一下,做题的目的是为了更加熟悉拓扑排序的算法思想,算法套路,不能停留在解决问题本身

感谢各位小伙伴们的 阅读点赞评论关注 ~ 希望本文能帮助到各位,共勉 ~

在这里插入图片描述

Smileyan
2025.04.12 19:04

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2333996.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【STM32】ST7789屏幕驱动

目录 CubeMX配置 配置SPI 开DMA 时钟树 堆栈大小 Keil工程配置 添加两个group 添加文件包含路径 驱动编写 写单字节函数 写字函数 写多字节函数 初始化函数 设置窗口函数 情况一&#xff1a;正常的0度旋转 情况二&#xff1a;顺时针90度旋转 情况三&#xff1…

10min速通Linux文件传输

实验环境 在Linux中传输文件需要借助网络以及sshd&#xff0c;我们可通过systemctl status sshd来查看sshd状态 若服务未开启我们可通过systemctl enable --now sshd来开启sshd服务 将/etc/ssh/sshd_config中的PermitRootLogin 状态修改为yes 传输文件 scp scp &#xff08;Sec…

dify windos,linux下载安装部署,提供百度云盘地址

dify1.0.1 windos安装包百度云盘地址 通过网盘分享的文件&#xff1a;dify-1.0.1.zip 链接: 百度网盘 请输入提取码 提取码: 1234 dify安装包 linux安装包百度云盘地址 通过网盘分享的文件&#xff1a;dify-1.0.1.tar.gz 链接: 百度网盘 请输入提取码 提取码: 1234 1.安装…

使用 TFIDF+分类器 范式进行企业级文本分类(二)

1.开场白 上一期讲了 TF-IDF 的底层原理&#xff0c;简单讲了一下它可以将文本转为向量形式&#xff0c;并搭配相应分类器做文本分类&#xff0c;且即便如今的企业实践中也十分常见。详情请见我的上一篇文章 从One-Hot到TF-IDF&#xff08;点我跳转&#xff09; 光说不练假把…

《车辆人机工程-汽车驾驶操纵实验》

汽车操纵装置有哪几种&#xff0c;各有什么特点 汽车操纵装置是驾驶员直接控制车辆行驶状态的关键部件&#xff0c;主要包括以下几种&#xff0c;其特点如下&#xff1a; 一、方向盘&#xff08;转向操纵装置&#xff09; 作用&#xff1a;控制车辆行驶方向&#xff0c;通过转…

python高级编程一(生成器与高级编程)

@TOC 生成器 生成器使用 通过列表⽣成式,我们可以直接创建⼀个列表。但是,受到内存限制,列表容量肯定是有限的。⽽且,创建⼀个包含100万个元素的列表,不仅占⽤很⼤的存储空间,如果我们仅仅需要访问前⾯⼏个元素,那后⾯绝⼤多数元素占 ⽤的空间都⽩⽩浪费了。所以,如果…

单片机Day05---动态数码管显示01234567

一、原理图 数组索引段码值二进制显示内容00x3f0011 1111010x060000 0110120x5b0101 1011230x4f0100 1111340x660110 0110450x6d0110 1101560x7d0111 1101670x070000 0111780x7f0111 1111890x6f0110 11119100x770111 0111A110x7c0111 1100B120x390011 1001C130x5e0101 1110D140…

muduo库源码分析: One Loop Per Thread

One Loop Per Thread的含义就是&#xff0c;一个EventLoop和一个线程唯一绑定&#xff0c;和这个EventLoop有关的&#xff0c;被这个EventLoop管辖的一切操作都必须在这个EventLoop绑定线程中执行 1.在MainEventLoop中&#xff0c;负责新连接建立的操作都要在MainEventLoop线程…

MCP结合高德地图完成配置

文章目录 1.MCP到底是什么2.cursor配置2.1配置之后的效果2.2如何进行正确的配置2.3高德地图获取key2.4选择匹配的模型 1.MCP到底是什么 作为学生&#xff0c;我们应该如何认识MCP&#xff1f;最近看到了好多跟MCP相关的文章&#xff0c;我觉得我们不应该盲目的追求热点的技术&…

重读《人件》Peopleware -(5)Ⅰ管理人力资源Ⅳ-质量—若时间允许

20世纪的心理学理论认为&#xff0c;人类的性格主要由少数几个基本本能所主导&#xff1a;生存、自尊、繁衍、领地等。这些本能直接嵌入大脑的“固件”中。我们可以在没有强烈情感的情况下理智地考虑这些本能&#xff08;就像你现在正在做的那样&#xff09;&#xff0c;但当我…

文献总结:AAAI2025-UniV2X-End-to-end autonomous driving through V2X cooperation

UniV2X 一、文章基本信息二、文章背景三、UniV2X框架1. 车路协同自动驾驶问题定义2. 稀疏-密集混合形态数据3. 交叉视图数据融合&#xff08;智能体融合&#xff09;4. 交叉视图数据融合&#xff08;车道融合&#xff09;5. 交叉视图数据融合&#xff08;占用融合&#xff09;6…

制造一只电子喵 (qwen2.5:0.5b 微调 LoRA 使用 llama-factory)

AI (神经网络模型) 可以认为是计算机的一种新的 “编程” 方式. 为了充分利用计算机, 只学习传统的编程 (编程语言/代码) 是不够的, 我们还要掌握 AI. 本文以 qwen2.5 和 llama-factory 举栗, 介绍语言模型 (LLM) 的微调 (LoRA SFT). 为了方便上手, 此处选择使用小模型 (qwen2…

Redis核心功能实现

前言 学习是个输入的过程&#xff0c;在进行输入之后再进行一些输出&#xff0c;比如写写文章&#xff0c;笔记&#xff0c;或者做一些技术串讲&#xff0c;虽然需要花费不少时间&#xff0c;但是好处很多&#xff0c;首先是能通过输出给自己的输入带来一些动力&#xff0c;然…

【连载3】基础智能体的进展与挑战综述

基础智能体的进展与挑战综述 从类脑智能到具备可进化性、协作性和安全性的系统 【翻译团队】刘军(liujunbupt.edu.cn) 钱雨欣玥 冯梓哲 李正博 李冠谕 朱宇晗 张霄天 孙大壮 黄若溪 2. 认知 人类认知是一种复杂的信息处理系统&#xff0c;它通过多个专门的神经回路协调运行…

MacOs java环境配置+maven环境配置踩坑实录

oracl官网下载jdk 1.8的安装包 注意可能需要注册&#xff01;&#xff01;&#xff01; 下载链接&#xff1a;下载地址点击 注意晚上就不要下载了 报错400 &#xff01;&#xff01;&#xff01; 1.点击安装嘛 2.配置环境变量 export JAVA_HOME/Library/Java/Java…

【Git】--- 企业级开发流程

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; Git 本篇博客我们讲解Git在企业开发中的整体流程&#xff0c;理解Git在实际企业开发中的高效设计。 &#x1f3e0; 企业级开发流程 一个软件从零开始到最…

蓝桥杯嵌入式历年省赛客观题

一.第十五届客观题 第十四届省赛 十三届 十二届

解决2080Ti使用节点ComfyUI-PuLID-Flux-Enhanced中遇到的问题

使用蓝大的工作流《一键同时换头、换脸、发型、发色之双pulid技巧》 刚开始遇到的是不支持bf16的错误 根据《bf16 is only supported on A100 GPUs #33》中提到&#xff0c;修改pulidflux.py中的dtype 为 dtype torch.float16 后&#xff0c;出现新的错误&#xff0c;这个…

LabVIEW驱动开发的解决思路

在科研项目中&#xff0c;常面临将其他语言开发的定制采集设备驱动转换为 LabVIEW 适用形式的难题。特别是当原驱动支持匮乏、开发人员技术支持不足时&#xff0c;如何抉择解决路径成为关键。以下提供具体解决思路&#xff0c;助力高效解决问题。 ​ 一、评估现有驱动死磕的可…

七、Qt框架编写的多线程应用程序

一、大纲 学习内容&#xff1a;使用两个线程&#xff0c;分别点击两个按钮&#xff0c;触发两个不同的效果 所需控件&#xff1a;两个button、三个label 涉及知识点&#xff1a;多线程、Qt的connect机制、定时器、互斥锁 需求&#xff1a; 1&#xff0c;多线程定时计数&#x…