拓扑排序基础

news2024/9/20 18:47:33
拓扑排序简要介绍及应用场景

拓扑排序:对图中所有节点进行排序,保证每个节点的前置节点都在这个节点之前。

【使用要求】:有向图,无环

拓扑排序的顺序可能不只一种。拓扑排序也可以用来判断图中有没有环存在。

拓扑排序步骤:

  1. 在图中找到所有入度为0的点 (入度为0说明此节点没有前置节点,那它只能是其他节点的前置节点,所以先提出来放在拓扑排序前面)
  2. 把所有入度为0的节点在图中删掉,重点是把与其他邻居节点的关联删除。之后继续找到入度为0的节点并删掉关联。
  3. 直到所有的节点被删除,依次删除的顺序就是正确的拓扑排序结果
  4. 如果无法删除所有点,说明有向图中有环存在

我们来看一道拓扑排序的模板题

P r o b l e m 1 Problem1 Problem1 课程表(2) LeetCode210

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

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

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

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

示例 3:

输入:numCourses = 1, prerequisites = []
输出:[0]

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= numCourses * (numCourses - 1)
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • ai != bi
  • 所有[ai, bi] 互不相同

问题分析:

这道题是拓扑排序的模板题,[ai,bi]可以转化为图中的bi节点指向ai节点,最终的拓扑排序结果就是题目要求的课程排序。

直接看代码:

vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
    vector<vector<int>> Graph(numCourses);
    int indegree[numCourses];
    memset(indegree, 0, sizeof(indegree));
    // 建图
    for (vector<int> vec : prerequisites) {
        Graph[vec[1]].push_back(vec[0]);
        indegree[vec[0]]++;
    }

    //用数组模拟队列
    vector<int> queue(numCourses);
    int left = 0;
    int right = 0;
    for(int i = 0;i < numCourses;i++){
        if(indegree[i] == 0){
            queue[right++] = i;
        }
    }
    int cnt = 0;
    while(left < right){
        int deleteNode = queue[left]; //被删除节点
        cnt++;
        //现在剔除被删除节点与邻居节点的关联,直接将邻居节点的入度-1
        for(int neighbor : Graph[deleteNode]){
            indegree[neighbor]--;
            if(indegree[neighbor] == 0){
                queue[right++] = neighbor;
            }
        }
        left++;
    }
    return cnt == numCourses ? queue: vector<int>();
}
代码分析:

在解决代码中,我们用了动态结构邻接表去存储图,值得一提的是,我们利用了int indgree[]作为入度表记录每个节点的入度,先遍历一遍indgree[]获取一开始入度为0的节点,将其存到队列queue中,直接用语言本身的queue比较耗时间,所以我们利用了数组去模拟队列,vector<int> queue(numCourses),发现入度为0节点,则在尾部加入此节点,同时队列尾部right++。再取出入度为0节点,同时删除其与邻居节点的关联,删除过程中如果发现邻居节点的入度减为0,则将邻居节点加入到队列中。删除节点完毕之后,left++继续剔除入度为0的节点。

最后我们用cnt记录删除了多少个节点,如果删除节点个数小于总节点个数,说明图中存在环。

我们再利用链式前向星建图的方式看一道相差无几的拓扑排序模板题

P r o b l e m 2 Problem2 Problem2 拓扑排序模板 牛客AB13

描述

给定一个包含n个点m条边的有向无环图,求出该图的拓扑序。若图的拓扑序不唯一,输出任意合法的拓扑序即可。若该图不能拓扑排序,输出−1。

输入描述:

第一行输入两个整数n,m ( 1≤𝑛,𝑚≤2⋅1051≤n,m≤2⋅105),表示点的个数和边的条数。
接下来的m行,每行输入两个整数 u i , v i u_i,v_i ui,vi(1≤u,v≤n),表示 u i u_i ui v i v_i vi之间有一条有向边。

输出描述:

若图存在拓扑序,输出一行n个整数,表示拓扑序。否则输出−1。

示例:

输入:

5 4
1 2
2 3
3 4
4 5

输出:

1 2 3 4 5

原理一模一样,值得一提的只有链式前向星找邻居节点,请看以下代码:

#include <cstring>
#include <iostream>
#include <vector>
using namespace std;

int head[200001];
int nextEdge[200001];
int toEdge[200001];
int cnt = 0;

void build() {
    cnt = 0;
    memset(head, 0, sizeof(head));
    memset(nextEdge, 0, sizeof(nextEdge));
    memset(toEdge, 0, sizeof(toEdge));
}

void addEdge(int u, int v) {
    cnt++;
    nextEdge[cnt] = head[u];
    toEdge[cnt] = v;
    head[u] = cnt;
}

void directGraph(vector<vector<int>>& Edges, int M) {
    for (int i = 0; i < M; i++) {
        addEdge(Edges[i][0], Edges[i][1]);
    }
}

int main() {
    int N, M;
    scanf("%d", &N);
    scanf("%d", &M);
    int indgree[N+1]; //入度表
    memset(indgree, 0, sizeof(indgree));
    vector<vector<int>> Edges(M, vector<int>(2));
    for (int i = 0; i < M; i++) {
        int from, to;
        scanf("%d", &from);
        scanf("%d", &to);
        Edges[i][0] = from;
        Edges[i][1] = to;
        indgree[to]++;
    }
    build();
    directGraph(Edges, M);

    vector<int> queue(N+1);
    int left = 1;
    int right = 1;
    for (int i = 1; i <= N; i++) {
        if (indgree[i] == 0) {
            queue[right++] = i;
        }
    }

    int count = 0;
    while (left < right) {
        int deleteNode = queue[left];
        count++;
        //删除关联
        for (int ei = head[deleteNode]; ei > 0; ei = nextEdge[ei]) {
            int neighbor = toEdge[ei];
            indgree[neighbor]--;
            if (indgree[neighbor] == 0) {
                queue[right++] = neighbor;
            }
        }
        left++;
    }
    if (count != N){
        printf("%d", -1);
        return 0;    
    }

    for (int i = 1; i <= N - 1; i++) {
        printf("%d ", queue[i]);
    }
    printf("%d",queue[N]);
    return 0;
}

这两道题目放一起看,还是邻接表用起来更舒服,写的快一些,不过遇到对空间严苛的题目,要用链式前向星建图法。

第三题会用到 小根堆 这个数据结构,同学们可以去【堆】专栏复习一下堆的知识

P r o b l e m 3 Problem3 Problem3 字典序的拓扑排序 洛谷U107394

题目描述

有向无环图上有n个点,m条边。求这张图字典序最小的拓扑排序的结果。字典序最小指希望排好序的结果中,比较靠前的数字尽可能小。

输入格式

第一行是用空格隔开的两个整数n和m,表示n个点和m条边。

接下来是m行,每行用空格隔开的两个数u和v,表示有一条从u到v的边。

输出格式

输出一行,拓扑排序的结果,数字之间用空格隔开

输入:

5 3
1 2
2 4
4 3

输出:

1 2 4 3 5

问题分析:

我们先重点看一下拓扑排序和字典序最小的概念:

  • 拓扑排序:在拓扑排序之后的序列中,每个节点的前置节点都在这个节点之前。
  • 字典序最小:序列中比较靠前的数字尽可能小,如果序列按照升序排序,那么它的字典序是最小的。

第一种思路:

​ 我们第一时间肯定是想着在拓扑排序的基础上再做一些操作,使字典序最小,还记得我们之前是怎么在草稿纸中上演算拓扑排序的吗?第一轮,我们把入度为0的数据挑出来,并删除它们在图中的关联;第二轮,把新的入度为0的数据挑出来,并删除它们在图中的关联;这样一轮一轮下去,最终得到完整的拓扑排序结果。

一开始我是这么想的,我用vector<int>去存储每一轮的结果,并把每一轮的结果按照升序排序,排序之后放到结果数组result中,这样一轮一轮下去,最终得到一个完整序列,具体操作请看下图:

在这里插入图片描述

​ 这个思路经验证之后,我们会发现这是不正确的,10和4没有先后关系,按照字典序最小的规则,4应该在10前面。为什么会存在这种错误呢?是因为我们是一轮一轮来排序的,10是第二轮中的数,4是第三轮的数,即使10和4没有前置关系,但是因为10比4轮次靠前,10就比4先出现。

第二种正确思路:

​ 回顾一下我们之前对拓扑排序的代码实现,我们是用队列来存储各入度为0的节点的,弹出一个入度为0的节点之后将由删除此节点而新产生的入度为0的节点加入到队列。注意,【我们是把前置节点先弹出队列(也就是先访问),之后再加入后续节点的】,只要保证这种操作方式我们得到的序列一定是拓扑排序序列。

​ 同时要保证字典序最小,贪心地想,弹出来的数越小越好嘛,最好是此时图中所有节点中数最小的点,我们借助【小根堆】就能得到最小的点。

​ 到现在解决方式已经很明了了,我们用小根堆代替队列来存储各入度为0的节点,同时保持【将前置节点弹出小根堆,再加入后续节点】操作。具体操作我也给出细节图:

在这里插入图片描述

解决代码:
#include <iostream>
#include <vector>
#include <cstring>
#include <queue>
using namespace std;

int head[101];
int nextEdge[1001];
int toEdge[1001];
int cnt = 0;

void build(){
    cnt = 0;
    memset(head,0,sizeof(head));
    memset(nextEdge,0,sizeof(nextEdge));
    memset(toEdge,0,sizeof(toEdge));
}

void addEdge(int u,int v){
    cnt++;
    nextEdge[cnt] = head[u];
    toEdge[cnt] = v;
    head[u] = cnt;
}

int main(){
    build();
    int N;
    scanf("%d",&N);
    vector<int> indgree(N+1, 0);
    for(int i = 1;i <= N;i++){
        int to;
        scanf("%d",&to);
        while(to != 0){
            addEdge(i,to);
            indgree[to]++;
            scanf("%d",&to);
        }
    }
    
    priority_queue<int,vector<int>,greater<int>> pq;
    //把初始入度为0的节点加入小根堆中
    for(int i = 1; i <= N;i ++){
        if(indgree[i] == 0){
            pq.push(i);
        }
    }
    
    vector<int> result;
    while(pq.size() != 0){
        int deleteNode = pq.top();
        result.push_back(deleteNode);
        pq.pop();
        for(int ei = head[deleteNode];ei > 0; ei = nextEdge[ei]){
            indgree[toEdge[ei]]--;
            if(indgree[toEdge[ei]] == 0){
                pq.push(toEdge[ei]);
            }
        }
    }
    
    for(int i = 0 ;i < N-1;i++){
        cout << result[i] << " ";
    }
    cout << result[N-1];
    
    return 0;
}

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

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

相关文章

【结构型】树形结构的应用王者,组合模式

目录 一、组合模式1、组合模式是什么&#xff1f;2、组合模式的主要参与者&#xff1a; 二、优化案例&#xff1a;文件系统1、不使用组合模式2、通过组合模式优化上面代码优化点&#xff1a; 三、使用组合模式有哪些优势1、统一接口&#xff0c;简化客户端代码2、递归结构处理方…

maxcompute使用篇

文章目录 maxcompute使用篇1.mongoDB与maxcompute 进行数据同步1.1 基本类型的数据1.2部分复杂类型的数据 2.maxcompute中复杂数据类型解析2.1 get_json_object2.2 json_tuple2.3 处理json几种失效的情况:2.4 STR_TO_MAP、MAP_KEYS2.5 regexp_replace2.6 FROM_JSON2.7 nvl2.8 t…

基于matlab的通信系统设计及仿真

文章目录 前言资料获取设计介绍功能介绍设计程序具体实现截图参考文献设计获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设…

PHP邮箱系统:从入门到实战搭建教程指南!

PHP邮箱系统配置教程&#xff1f;如何选用合适的PHP邮箱系统库&#xff1f; 为了满足个性化和定制化的需求&#xff0c;许多开发者选择使用PHP来搭建自己的邮箱系统。AokSend将带你从入门到实战&#xff0c;详细介绍如何搭建一个功能完善的PHP邮箱系统。 PHP邮箱系统&#xf…

谈谈你对线程池的了解

一、什么是线程池 线程池是一种创建和管理线程的技术。 二、怎么创建线程池 通过Executors工具类的静态方法&#xff0c;创建线程池。创建ThreadPoolExecutor对象&#xff0c;按照业务需要&#xff0c;自定义线程参数&#xff0c;创建线程池。 三、线程池的状态有哪些 线程池的…

VMware vCenter Server 8.0U3b 发布下载,新增功能概览

VMware vCenter Server 8.0U3b 发布下载&#xff0c;新增功能概览 Server Management Software | vCenter 请访问原文链接&#xff1a;https://sysin.org/blog/vmware-vcenter-8-u3/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysi…

VirtualBox7.1.0 安装 Ubuntu22.04.5 虚拟机

环境 &#xff08;1&#xff09;宿主机系统&#xff1a;Windows10 &#xff08;2&#xff09;虚拟机软件&#xff1a;VirtualBox7.1.0 &#xff08;3&#xff09;虚拟机系统&#xff1a;Ubuntu 22.04.5 LTS (Jammy Jellyfish) 步骤 &#xff08;1&#xff09;第一步 &…

Python基础(七)——PyEcharts数据分析(面向对象版)

四、使用PyEcharts数据分析案例&#xff08;面向对象版&#xff09; 【前言&#xff1a;为了巩固之前的Python基础知识&#xff08;一&#xff09;到&#xff08;五&#xff09;&#xff0c;并为后续使用Python作为数据处理的好帮手&#xff0c;我们一起来用面向对象的思想来理…

并发编程 - 锁(@synchronized)

引言 在前面的博客中&#xff0c;我们已经讨论了锁在多线程编程中的重要性&#xff0c;主要是为了解决多线程同时访问同一片共享数据时发生的竞争条件&#xff08;race conditions&#xff09;&#xff0c;导致数据不一致和崩溃问题。 并且介绍了一个在Objective-C中&#xf…

mysql笔记7(单表查询)

文章目录 1. select① 从伪表里查数据(可以结合第3点dual理解数据来源)select 文字 ;做计算&#xff1a;select 算式;select 文字( 或算式) as 别名;(as 用于起别名&#xff0c;可省略) ② 从真实表里查数据select * from 表名;select 字段名&#xff0c;字段名 from 表名; 2. …

centos远程桌面连接windows

CentOS是一款广泛使用的Linux发行版&#xff0c;特别是在服务器领域。很多企业和个人用户会选择远程连接到CentOS进行操作和维护。虽然CentOS自带了一些远程桌面解决方案&#xff0c;但它们在使用上存在一些局限性。接下来&#xff0c;我将介绍如何实现CentOS的远程桌面连接&am…

关于wordPress中的用户登录注册等问题

前言 大家在做类似的功能的时候&#xff0c;有没有相关的疑问。那就是我都已经选择好了相应的主题和模版&#xff0c;但是为什么都没有用户注册和用户登录的页面存在呢&#xff1f; WordPress默认情况下不提供用户注册和登录功能的原因是它最初是作为一个博客平台开发的&…

OCR两篇革命之作

DocOwl2 参考 阿里8B模型拿下多页文档理解新SOTA&#xff0c;324个视觉token表示一页&#xff0c;缩减80% mPLUG-DocOwl 2聚焦多页文档理解&#xff0c;兼顾效果和效率&#xff0c;在大幅缩减单页视觉token的前提下实现了多页文档理解的SOTA效果。 仅用324个token表示文档图…

离散制造 vs 流程制造:锚定精准制造未来,从装配线到化学反应,实时数据集成在制造业案例中的多维应用

使用 TapData&#xff0c;化繁为简&#xff0c;摆脱手动搭建、维护数据管道的诸多烦扰&#xff0c;轻量替代 OGG, Kettle 等同步工具&#xff0c;以及基于 Kafka 的 ETL 解决方案&#xff0c;「CDC 流处理 数据集成」组合拳&#xff0c;加速仓内数据流转&#xff0c;帮助企业…

使用雷达速度因子进行越野导航的鲁棒高速状态估计

使用雷达速度因子进行越野导航的鲁棒高速状态估计 Morten Nissov 1 , 2 ^{1,2} 1,2, Jeffrey A. Edlund 1 ^{1} 1, Patrick Spieler 1 ^{1} 1, Curtis Padgett 1 ^{1} 1, Kostas Alexis 2 ^{2} 2 和 Shehryar Khattak 1 ^{1} 1 摘要 在复杂环境中实现机器人自主性以用于关键…

【限流算法】

文章目录 介绍算法原理适用场景令牌通算法实现限流算法 介绍 令牌桶算法是网络流量整形&#xff08;Traffic Shaping&#xff09;和速率限制&#xff08;Rate Limiting&#xff09;中最常使用的一种算法。典型情况下&#xff0c;令牌桶算法用来控制发送到网络上的数据的数目&a…

第6天:趋势轮动策略开发(年化18.8%,大小盘轮动加择时)

原创内容第655篇&#xff0c;专注量化投资、个人成长与财富自由。 轮动策略是一种投资策略&#xff0c;它涉及在不同的资产类别、行业或市场之间进行切换&#xff0c;以捕捉市场机会并优化投资组合的表现。 这种策略的核心在于识别并利用不同资产或市场的相对强弱&#xff0c…

[数据集][目标检测]智慧养殖场肉鸡目标检测数据集VOC+YOLO格式3548张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;3548 标注数量(xml文件个数)&#xff1a;3548 标注数量(txt文件个数)&#xff1a;3548 标注…

医学数据分析实训 项目九 糖尿病风险预测

文章目录 综合实践二 糖尿病遗传风险预测一、分析目标二、实现步骤三、数据准备四、特征工程五、模型构建六、性能度量七、提交要求 综合实践任务二 糖尿病遗传风险预测代码&#xff08;一&#xff09;数据准备&#xff08;二&#xff09;特征工程&#xff08;三&#xff09;模…

Selenium通过ActionBuilder模拟鼠标操作直接移动到指定坐标的注意事项

在目前&#xff08;2024-09-18&#xff09;得Selenium官方手册中&#xff0c;模拟鼠标操作基本上都是通过ActionChains完成的&#xff0c;唯独有一动作&#xff0c;是通过ActionBuilder完成的。 而前者ActionChains&#xff0c;主要是通过offset&#xff0c;也就是坐标偏移量来…