拓扑排序详解

目录

一、拓扑排序前置知识

1.1 定义:

1.2 AOV网:

二、如何拓扑排序

2.1 运用 kahn 算法:

2.2 实现拓扑排序:

2.3 拓扑排序的应用:

2.4 拓扑排序编写模板:

三、例题练习

3.1 例题1:课程表

3.2 例题2:课程表II

3.3 例题3:火星词典

四、时间复杂度分析及合理性证明

4.1 时间复杂度:

4.2 合理性证明:


一、拓扑排序前置知识

1.1 定义:

拓扑排序主要用来解决有向无环图(DAG 图)的所有节点的排序。简单来说就是找到做事情的先后顺序,拓扑排序的结果可能不是唯一的。

我们可以那我们日常上学的例子来描述这个过程,全部的步骤有【起床】、【洗漱】、【吃饭】、【穿衣服】、【整理书包】、【上学去】等,要想完成洗漱和穿衣服的后续操作就必须要先起床,起床后做洗漱和穿衣服都可以(拓扑排序的结果不唯一),最后上学的时候必须要把前面那 5 步都执行完毕才可以去上学。这就是拓扑排序的过程。

这里需要先了解什么是入度,什么是出度。

入度:是以 v 为终点的有向边的条数。

出度:是以 v 为起始点的有向边的条数。

1.2 AOV网:

AOV网:顶点活动图。在有向无环图中,用顶点来表示一个活动,用边来表示活动的先后顺序的图结构。

在日常生活中,我们进行的活动都可以看作是由若干个子活动组成的集合,这些活动之间必定存在一定的先后顺序,即某些子活动必须在其他的一些子活动完成后才能开始。

我们用有向图来表现子活动之间的先后关系,子活动之间的先后关系为有向边,这种有向图称为顶点活动网络,即 AOV 网(Activity On Vertex Network)。一个 AOV 网必定是一个有向无环图,即不带有回路

二、如何拓扑排序

2.1 运用 kahn 算法:

没学过没关系,我们就是要讲这个。

• 过程:

初始状态下,集合 S装着所有入度为 0 的点,L 是一个空列表。

每次从 S 中取出一个点 u(可以随便取)放入 L, 然后将 u 的所有边 (u,v1),(u,v2),(u,v3)..... 删除。对于边 (u,v),若将该边删除后点 v 的入度变为 0,则将 v 放入 S 中。

不断重复以上过程,直到集合 S 为空。检查图中是否存在任何边,如果有,那么这个图一定有环路,否则返回 L,L 中顶点的顺序就是构造拓扑序列的结果。

上面就是 kahn 算法是实现过程。下面为了减少友友的学习成本,我在下面给出在代码中我们具体要如何实现。

2.2 实现拓扑排序:

借助队列来一次 BFS 即可。

• 初始化:把所有入度为 0 的点加入到队列中。

• 当队列不为空的时候:

1. 拿出队头元素,加入到最终结果中。

2. 删除与该元素相连的边。

3. 判断:与删除边相连的点,是否入度变为 0 ,如果入度为 0 ,加入到队列中。

到这里有一个致命的问题:如何建图?如何在图上进行上述删除节点的操作?

我们要灵活的使用语言提供的容器(c 语言的话当我没说) ,建图无非就两种,一种是邻接矩阵,另一种是邻接表。拓扑排序采用邻接表会更加方便一些,我们可以使用 List<List<Integer>> edges(局部);或者 Map<Integer,List<Integer>> edges(通用); 完成建表。

最后我们使用一个 int 类型的数组来统计各个节点的入度。这里如果节点不是数字的话,我们要想办法把它转化成数字,例如如果节点是字符串的话,我们可以自己构造一个哈希函数,来把字符串转化成数字。

例如上图:

第一步删去入度为 0 点(【起床】)及其对应的边并将其加入到结果中,这样【洗漱】和【穿衣服】就成了入度为 0 的点。

接着将【洗漱】和【穿衣服】入队,因为入度都为 0 所以谁先谁后都可以。

接着就是重复上面的操作,【上学去】必须是最后一个。

最后排序的结果如上图。

2.3 拓扑排序的应用:

拓扑排序可以用来判断有向图中是否有环。

2.4 拓扑排序编写模板:

拓扑排序:

1. 准备工作:

• 建立入度表。

• 建立edge(1.Map,2.List)。

2. 建表:

建表时入度要记得 ++ 。

3. 拓扑排序:

拓扑排序前要把入度为 0 的节点进入队列。

三、例题练习

说了那么多,我们来几题练练手。

3.1 例题1:课程表

• 题目链接:课程表

• 问题描述:

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

• 解题思路:

原问题可以转换成⼀个拓扑排序问题。 用 BFS 解决拓扑排序即可。根据 2.4 的编写模板来编写代码,这是第一题,可以直接看,代码是如何实现的,拓扑排序其实也是很固定的那一套写法,写多了就觉得就那样啦。

• 代码编写:

class Solution {
    public boolean canFinish(int n, int[][] p) {
        //1.准备工作
        int[] in = new int[n];//存储入度
        Map<Integer,List<Integer>> edges = new HashMap<>();//存储边
        //2.建图
        for(int[] tmp:p){
            int a = tmp[0],b = tmp[1];
            if(!edges.containsKey(b)){
                edges.put(b,new ArrayList<>());//建立边的关系
            }
            edges.get(b).add(a);//一个点不止只有一条边
            in[a]++;
        }
        //3.拓扑排序
        Queue<Integer> q = new LinkedList<>();
        for(int i = 0;i < n;i++){
            if(in[i] == 0){
                q.add(i);
            }
        }
        while(!q.isEmpty()){
            int t = q.poll();
            for(int x:edges.getOrDefault(t,new ArrayList<>())){
                in[x]--;
                if(in[x] == 0){//入度为 0 进入队列
                    q.add(x);
                }
            }
        }
        //4.判环
        for(int x:in){//如果有的节点入度不为 0 说明有环
            if(x != 0){
                return false;
            }
        }
        return true;
    }
}

3.2 例题2:课程表II

• 题目链接:课程表II

• 问题描述: 

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

• 解题思路:

和上面的课程表1基本一模一样,可以拿这题练练手,唯一区别就是要把遍历结果,存储下来。

下面的代码编写是用 List<List<Integer>>  来编写的,没有什么特别的好处,主要是想给友友们展示一下两种写法。

• 代码编写:

class Solution {
    public int[] findOrder(int n, int[][] p) {
        //拓扑排序
        //1.准备
        int[] in = new int[n];//入度
        int[] ans = new int[n];//最后的答案
        int k = 0;
        List<List<Integer>> edges = new ArrayList<>();
        for(int i = 0;i < n;i++){
            edges.add(new ArrayList<>());//这里必须要先创建,下标必须要从0到n,这一点其实我不是很喜欢
        }
        //2.建表
        for(int i = 0;i < p.length;i++){
            int a = p[i][0],b = p[i][1];
            //如果 b 不存在开辟一个
            edges.get(b).add(a);
            in[a]++;//注意对应入度要增加
        }
        //3.拓扑排序
        Queue<Integer> q = new LinkedList<>();
        for(int i = 0;i < n;i++){
            if(in[i] == 0){
                q.add(i);//入的是下标,所以不能使用foreach
            }
        }
        while(!q.isEmpty()){
            int t = q.poll();
            ans[k++] = t;
            for(int x:edges.get(t)){
                in[x]--;
                if(in[x] == 0){
                    q.add(x);
                }
            }
        }
        return n == k ? ans : new int[0];
    }
}

稍微上点强度。 

3.3 例题3:火星词典

• 题目链接:火星词典

• 问题描述:

现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。

给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序 。

请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。

字符串 s 字典顺序小于 字符串 t 有两种情况:

  • 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
  • 如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。

• 解题思路:

本题的难点在于题意的理解和如何收集信息(如何建图),我们可以使用两层 for 循环枚举出所有的两个字符串的组合,然后利用双指针,根据字典序规则找出信息。in 这里采用 Map 数据结构来存储,注意一定要初始化,不然就不会存在入度为 0 的节点,edges 里面的 value 要使用 Set 来去重,拓扑排序有重复的边,会出错的。

注意:本题有个非常恶心的地方,题目给出的条件有可能是错误的,但是题目没有说明,如果发现条件是错误的话直接返回空串即可。

• 代码编写:

class Solution {
    public static String alienOrder(String[] words) {
        //拓扑排序
        //1.准备工作
        int n = words.length;
        Map<Character,Integer> in = new HashMap<>();//记录入度,必须要初始化,不然就没有入度为 0 的节点
        for(int i = 0;i < words.length;i++){
            for(int j = 0;j < words[i].length();j++){
                in.put(words[i].charAt(j),0);//初始化入度点
            }
        }
        Map<Character,Set<Character>> edges = new HashMap<>();//使用 Set 去重
        StringBuilder sd = new StringBuilder();
        //2.建表
        for(int i = 0;i < n;i++){
            for(int j = i + 1;j < n;j++){
                char[] a = words[i].toCharArray();
                char[] b = words[j].toCharArray();
                int k = 0;
                for(k = 0;k < a.length && k < b.length;k++){
                    if(a[k] != b[k]){
                        if(!edges.containsKey(a[k])){
                            edges.put(a[k],new HashSet<>());//a 是在前面的节点
                        }
                        if(!edges.get(a[k]).contains(b[k])){
                            edges.get(a[k]).add(b[k]);//建表
                            in.put(b[k],in.get(b[k]) + 1);//入度
                        }
                        break;//只要第一个不能的字母
                    }
                }
                if(k == a.length || k == b.length){
                    if(a.length > b.length){//这里非常恶心,因为能走到这里说明,前面的
                    //字母都相等,谁长谁大,if的这种情况 a 长,理应 a 大,但是 a 排在
                    //前面说明题目给出的字符串是错误的直接返回空字符串。(题目没有说明
                    //给出的条件存在不合法,所以这里我错了很多次)
                        return "";
                    }
                }
            }
        }

        //3.拓扑排序
        Queue<Character> q = new LinkedList<>();
        for(Map.Entry<Character,Integer> entry:in.entrySet()){// map
            if(entry.getValue() == 0){
                q.add(entry.getKey());
            }
        }
        //正常的拓扑排序,无非就是数字变成了字符而已。
        while(!q.isEmpty()){
            Character tmp = q.poll();
            sd.append(tmp);
            for(Character x:edges.getOrDefault(tmp,new HashSet<>())){
                in.put(x,in.get(x) - 1);
                if(in.get(x) == 0){
                    q.add(x);
                }
            }
        }
        //最后要判断有没有环
        for(char ch:in.keySet()){
            if(in.get(ch) != 0){
                return "";
            }
        }
        String ans = sd.toString();
        return ans;
    }
}

四、时间复杂度分析及合理性证明

4.1 时间复杂度:

拓扑排序的时间复杂度是 O(V + E),其中 V 是顶点数,E 是边数。很显然拓扑排序是遍历所有节点及边,所以时间复杂度就是定点数 + 边数。这个倒是好理解。

4.2 合理性证明:

如果一张图在删除掉入度为 0 的节点后,新图如果可以拓扑排序的话,那么原图一定也可以。反过来,如果原图可以拓扑排序,那么在删除掉后的新图也可以。

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

浙江大爱遮阳新材料股份有限公司新品发布会圆满成功

5月29日,浙江大爱遮阳新材料股份有限公司新品发布会在上海国家会展中心举办。本次会议出席的嘉宾有浙江大爱遮阳新材料股份有限公司总经理俞彬军,常务副总王志华,上海大爱益可美遮阳科技有限公司总经理陆俊青,浙江大爱遮阳新材料股份有限公司销售经理平鸿烈,销售经理蒋扬锋和玛…

【Python】轻松打包:CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南

【Python】轻松打包&#xff1a;CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南 大家好 我是寸铁&#x1f44a; 总结了一篇【Python】轻松打包&#xff1a;CentOS7上使用PyInstaller将Shell脚本转换为可执行文件的完美指南✨ 喜欢的小伙伴可以点点关注 &#…

C# 生成解决方案时出现的一些异常及解决方法

一、ResolveAssemblyReference任务意外失败 在使用VS2022生成C#解决方案时&#xff0c;出现如下错误&#xff1a; 解决方法&#xff1a; 项目的依赖项出现问题&#xff0c;重新更新一下依赖项即可 二、生成Win32资源时出错 产生这个原因的主要原因是配置的应用程序的图标文…

Thesios: Synthesizing Accurate Counterfactual I/O Traces from I/O Samples——论文泛读

ASPLOS 2024 Paper 论文阅读笔记整理 问题 在设计大规模分布式存储系统时&#xff0c;I/O活动的建模至关重要。具有代表性的/O跟踪&#xff0c;可以对现有硬件、配置和策略进行详细的性能评估。假设跟踪进一步支持分析假设情况&#xff0c;例如部署新的存储硬件、更改配置和修…

【机器学习】机器学习在深度学习领域中的作用:半监督学习的视角

&#x1f440;时空之门&#x1f440; &#x1f50d;引言&#x1f388;半监督学习概述&#x1f69d;机器学习在深度学习领域中的作用☘特征提取与表示学习&#x1f340;复杂任务建模❀结合半监督学习提升性能 &#x1f680;半监督学习在深度学习中的应用场景&#x1f4d5;图像识…

使用import语句导入模块

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 创建模块后&#xff0c;就可以在其他程序中使用该模块了。要使用模块需要先以模块的形式加载模块中的代码&#xff0c;这可以使用import语句实现。im…

智能体应用开发:构建各类垂直领域的ai智能体应用

最近在做个类似的项目&#xff0c;有用到这方面的知识&#xff0c;顺便做一些记录和笔记吧&#xff0c;希望能帮到大家了解智能体应用开发 目录 引言 AI原生应用的兴起 智能体在AI中的角色 实现原理详解 机器学习基础 数据管理与关联数据库 数据结构 Embedding 检索方…

CSS - 元素竖向百分比的基准值是什么?

例如有一个外层DIV元素&#xff0c;设定width为500px&#xff0c;height为300px。然后在其内部添加一个DIV元素&#xff0c;这个时候&#xff0c;内部的DIV元素&#xff0c;如果设定height margin-top padding-top 百分比之后&#xff0c;他们的百分比基准值是什么呢&#xff1…

基于JSP的母婴用品网站系统

你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果有需求可以文末加我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 工具&#xff1a;IDEA/Eclipse、Navicat、Maven 系统展示 首页 管理员功能界面 用户功能界面 前台首页功能界面 …

2024-6-4 石群电路-23

2024-6-4&#xff0c;星期二&#xff0c;13:16&#xff0c;天气&#xff1a;晴&#xff0c;心情&#xff1a;晴。今天又是阳光明媚的一天&#xff0c;没有什么特别的事情发生&#xff0c;加油学习喽~ 今日观看了石群老师电路课程的第39和第40个视频&#xff0c;继续第九章的学…

C语言笔记23 •文件操作•

1.为什么要使用文件&#xff1f; 文件&#xff0c;顾名思义就是存储我们所写在电脑上的文本内容。如果没有⽂件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失 了&#xff0c;等再次运⾏程序&#x…

视频如何转换成音频?音视频转换,4个方法

在当今数字化时代&#xff0c;我们常常需要处理各种不同格式的音视频文件。可能您有一个视频文件&#xff0c;但是您需要它的音频部分&#xff0c;或者您有一个音频文件&#xff0c;但您希望将其转换为视频格式。 无论您的需求是什么&#xff0c;音视频转换已经成为我们数字生…

人脸识别系统之动态人脸识别

二&#xff0e;动态人脸识别 1.摄像头人脸识别 1.1.导入资源包 import dlib import cv2 import face_recognition from PIL import Image, ImageTk import tkinter as tk import os注&#xff1a;这些导入语句允许您在代码中使用这些库和模块提供的功能&#xff0c;例如创建…

联邦学习数据集划分Dirichlet划分法及其可视化

文章目录 前言图片效果&#xff1a;独立同分布效果非独立同分布效果 一、参数输入输出 二、代码可视化:标签划分&#xff1a;代码调用 前言 用于实现并控制联邦学习客户端之间数据集非独立同分布&#xff0c;并将效果可视化 图片效果&#xff1a; 独立同分布效果 对不同类别…

python中的循环控制语句break与continue

学习这两个语句之前&#xff0c;我们要先了解这两个语句是什么意思&#xff1a; break&#xff1a;中断、打破的意思。所以它的跳出循环的意思 continue&#xff1a;继续的意思&#xff0c;意思是跳过当前条件&#xff0c;继续循环 新需求来了&#xff01;我们不仅要告诉 Py…

运营干货:用户运营体系

1、用户生命周期 2、用户引入阶段 3、用户留存阶段 4、用户回流阶段

Camunda BPM架构

Camunda BPM既可以单独作为流程引擎服务存在,也能嵌入到其他java应用中。Camunda BPM的核心流程引擎是一个轻量级的模块,可以被Spring管理或者加入到自定义的编程模型中,并且支持线程模型。 1,流程引擎架构 流程引擎由多个组件构成,如下所示: API服务 API服务,允许ja…

创意KMS知识图谱ui设计合集来了

创意KMS知识图谱ui设计合集来了

Redis的一致性

一、产生的原因 使用缓存&#xff0c;在进行写操作的时候就会出现不一致的问题。 一致性分为三类&#xff1a;强一致性&#xff0c;弱一致性&#xff0c;最终一致性 二、方案 2.1 延时双删 在更新数据库的操作前后分别进行一次删除缓存的操作&#xff0c;并在更新数据库之后…

广工电工与电子技术实验报告-按键键值识别和LED数码管显示

实验代码 Key_LED.v module Key_LED (key, HEX0); input[3:0] key; output[6:0] HEX0; reg[3:0] A; always (key) begin if (key[0] < 0) begin A 4b0001; end else if (key[1] < 0) begin A 4b0010; end else if (key[2] < 0) begin A 4b0011; end else if (key[…