数据结构与算法-图论-拓扑排序

news2025/3/18 9:13:31

前置芝士

概念

拓扑排序(Topological Sorting)是对有向无环图(DAG,Directed Acyclic Graph)的顶点进行排序的一种算法。它将图中的所有顶点排成一个线性序列,使得对于图中的任意一条有向边 (u, v),顶点 u 在序列中都出现在顶点 v 之前。也就是说,拓扑排序的结果反映了图中顶点之间的先后依赖关系。

需要注意的是,只有有向无环图才有拓扑排序,因为如果图中存在环,就无法满足先后顺序的要求。

算法流程

Kahn 算法(基于入度的算法)

-1统计入度:遍历图中的所有边,统计每个顶点的入度(即指向该顶点的边的数量)。

-2初始化队列:将所有入度为 0 的顶点加入一个队列中。

-3拓扑排序过程

-从队列中取出一个顶点,并将其加入拓扑排序的结果序列中。

-对于该顶点的所有邻接顶点,将它们的入度减 1。

-如果某个邻接顶点的入度变为 0,则将其加入队列。

-4检查结果:重复步骤 3,直到队列为空。如果最终拓扑排序结果序列中的顶点数量等于图中的顶点数量,则说明图是有向无环图,得到的序列就是拓扑排序结果;否则,图中存在环,不存在拓扑排序。

具体的 C++ 算法

#include <iostream>

#include <vector>

#include <queue>

using namespace std;

// 拓扑排序函数

vector<int> topologicalSort(int n, vector<vector<int>>& graph) {

    // 存储每个顶点的入度

    vector<int> inDegree(n, 0);

    // 存储拓扑排序的结果

    vector<int> result;

    // 队列用于存储入度为 0 的顶点

    queue<int> q;

    // 统计每个顶点的入度

    for (int i = 0; i < n; ++i) {

        for (int neighbor : graph[i]) {

            ++inDegree[neighbor];

        }

    }

    // 将所有入度为 0 的顶点加入队列

    for (int i = 0; i < n; ++i) {

        if (inDegree[i] == 0) {

            q.push(i);

        }

    }

    // 拓扑排序过程

    while (!q.empty()) {

        int u = q.front();

        q.pop();

        result.push_back(u);

        // 遍历该顶点的所有邻接顶点

        for (int v : graph[u]) {

            --inDegree[v];

            if (inDegree[v] == 0) {

                q.push(v);

            }

        }

    } 

    // 如果拓扑排序结果的长度不等于顶点数量,说明图中存在环

    if (result.size() != n) {

        return {}; // 返回空向量表示不存在拓扑排序

   }

return result;

}

int main() {

    int n = 6; // 顶点数量

    vector<vector<int>> graph(n);

    // 添加有向边

    graph[5].push_back(2);

    graph[5].push_back(0);

    graph[4].push_back(0);

    graph[4].push_back(1);

    graph[2].push_back(3);

    graph[3].push_back(1);

    vector<int> result = topologicalSort(n, graph);

    if (result.empty()) {

        cout << "图中存在环,不存在拓扑排序。" << endl;

    } else {

        cout << "拓扑排序结果:";

        for (int vertex : result) {

            cout << vertex << " ";

        }

        cout << endl;

    }

return 0;

}

应用场景

任务调度:在项目管理中,任务之间可能存在先后依赖关系。可以将任务看作图的顶点,任务之间的依赖关系看作有向边,通过拓扑排序可以确定任务的执行顺序,确保在执行某个任务之前,其所有前置任务都已经完成。

课程安排:在学校的课程体系中,某些课程可能需要先修其他课程。可以将课程看作顶点,先修关系看作有向边,使用拓扑排序可以得到一个合理的课程学习顺序。

编译顺序:在软件开发中,源文件之间可能存在依赖关系。通过拓扑排序可以确定源文件的编译顺序,确保在编译某个文件之前,其所有依赖的文件都已经编译完成。

数据流分析:在计算机科学中,数据流图可以表示数据在系统中的流动和处理过程。拓扑排序可以用于确定数据处理的顺序,确保数据在被处理之前已经被正确生成。

练习题:

家谱图:

标准的板子题:

代码:

#include<bits/stdc++.h>

using namespace std;

unordered_map<int,vector<int>>e;

const int N=110;

queue <int> Q;

int in[N];

int n;

void toposort() {

for(int i = 1; i <= n; i++) {

if(in[i] == 0) {

printf("%d ", i);

Q.push(i);

}

}

while(Q.size()) {

int u = Q.front(); Q.pop();

for(int v:e[u]) {

in[v]--;

if(in[v] == 0) {

printf("%d ", v);

Q.push(v);

}

}

}

}

int main(){

    cin>>n;

    for(int i=1;i<=n;i++){

        int tmp=0;

        while(cin>>tmp,tmp!=0){

            e[i].push_back(tmp);

            in[tmp]++;

        }

    }

    toposort();

    

}

奖金

分析:

差分约束的简化版,这里使用拓普排序,如果u->v,那么f[v]=max(f[v],f[u]+1);

为什么是max?因为还有其他的u1连接到了v,万一u1的值更大,那么v就要满足>u1

所以要求的是满足一系列v>u,u1,u2....的v的最小值

至于如果把100搞定,当然可以直接把初值设置为100或者答案最后加100就好了

同时归纳下:

如果边权无限制,只有使用差分约束了

如果边权非负,那就可以使用tarjan算法

如果边权为正数,就可以使用拓扑排序来做了!

代码

可达性统计:

分析:


求拓扑序,然后倒序求出f[i],这里的f[i]表示i之后可达的点的集合

f[i]=所以i的子节点能到的点的并集,这里的集合我们用string来模拟01001的二进制串来表示

代码:

#include<bits/stdc++.h>

using namespace std;

// 定义常量 N 作为最大顶点数

const int N = 30010;

// n 表示图的顶点数量,m 表示图的边数量

int n, m;

// in 数组用于记录每个顶点的入度,入度即指向该顶点的边的数量

int in[N];

// q 数组作为队列使用,用于拓扑排序过程中存储入度为 0 的顶点

int q[N];

// h 是队列的头指针,初始化为 0;t 是队列的尾指针,初始化为 -1

int h = 0, t = -1;

// f 数组是一个 bitset 数组,f[i] 表示从顶点 i 出发能够到达的所有顶点的集合// bitset 是一种高效存储二进制位的数据结构,这里用它可以方便地进行位运算

bitset<N> f[N];

// e 是一个无序映射,用于存储图的邻接表// e[u] 存储的是从顶点 u 出发的所有边指向的顶点

unordered_map<int, vector<int>> e;

// 拓扑排序函数,用于对有向图进行拓扑排序

bool toposort() {

    // 遍历所有顶点,将入度为 0 的顶点加入队列

    for (int i = 1; i <= n; i++) {

        if (!in[i]) {

            // 入度为 0 的顶点入队,尾指针后移

            q[++t] = i;

        }

    }

    // 当队列不为空时,持续进行处理

    while (h <= t) {

        // 取出队头元素

        int u = q[h++];

        // 遍历从顶点 u 出发的所有边指向的顶点

        for (int v : e[u]) {

            // 将顶点 v 的入度减 1,表示移除从 u 到 v 的边

            --in[v];

            // 如果顶点 v 的入度变为 0,将其加入队列

            if (!in[v]) {

                q[++t] = v;

            }

        }

    }

    // 这里原代码直接返回 1,存在问题,正确应该判断是否拓扑排序成功

    // 若拓扑排序成功,队列中应该存储了所有顶点,即 t == n - 1

return t == n - 1;

}

int main() {

    // 读取图的顶点数量 n 和边的数量 m

    scanf("%d%d", &n, &m);

    // 循环读取每条边的信息

    for (int i = 0; i < m; i++) {

        int u, v;

        // 读取边的起点 u 和终点 v

        scanf("%d%d", &u, &v);

        // 将终点 v 添加到起点 u 的邻接表中

        e[u].push_back(v);

        // 终点 v 的入度加 1

        in[v]++;

    }

    // 调用拓扑排序函数

    if (!toposort()) {

        // 如果拓扑排序失败,说明图中存在环,输出错误信息并退出程序

        cerr << "图中存在环,无法进行可达性统计。" << endl;

        return 1;

    }

    // 按照拓扑排序结果的逆序遍历顶点

    for (int i = n - 1; i >= 0; i--) {

        // 取出当前处理的顶点

        int u = q[i];

        // 顶点 u 能到达自身,将 f[u] 的第 u 位设为 1

        f[u][u] = 1;

        // 遍历从顶点 u 出发的所有边指向的顶点

        for (int v : e[u]) {

            // 通过位运算将 f[v] 能到达的顶点合并到 f[u] 中

            f[u] |= f[v];

        }

    }

    // 遍历所有顶点,输出每个顶点能到达的顶点数量

    for (int i = 1; i <= n; i++) {

        // count() 方法返回 bitset 中 1 的个数,即能到达的顶点数量

        cout << f[i].count() << endl;

    }

return 0;

}

车站分级:

分析

这道题可以通过建立有向图并进行拓扑排序来求解火车站最少的级别数,以下是具体思路:

建立有向图思路

顶点含义:将每个火车站看作图中的一个顶点,编号为 1 到 n ,对应图中的 n 个顶点。

边的构建:对于每一趟车次,设其停靠站为 si1​,si2​,⋯,sisi​​(按编号从小到大排列)。对于这趟车次的任意两个停靠站 sij​ 和 sik​(j<k),在图中添加有向边 (sij​,sik​)。这是因为根据题目条件,如果一趟车次停靠了火车站 x,则始发站、终点站之间所有级别大于等于火车站 x 的都必须停靠,添加边表示了这种级别上的 “约束” 关系。更通俗地理解,就是停靠靠前的车站可以 “指向” 停靠靠后的车站,意味着靠前车站的级别小于等于靠后车站的级别。

边的隐含意义:有向边 (u,v) 表示火车站 u 的级别小于等于火车站 v 的级别。通过这样构建有向图,就将车次运行情况转化为了图中顶点之间的关系。

拓扑排序思路

入度统计:在构建好有向图后,统计每个顶点(即每个火车站)的入度,入度表示有多少条边指向该顶点。

初始处理:把所有入度为 0 的顶点(即没有其他顶点的级别小于它的火车站)加入队列。这些顶点可以看作是最低级别的候选者。

拓扑排序过程:

从队列中取出一个顶点,这个顶点代表的火车站可以被划分为一个级别。

对于该顶点的所有邻接顶点(即由该顶点出发的边指向的顶点),将它们的入度减 1。这是因为已经处理了当前顶点,与它相关的 “约束” 减少了。

如果某个邻接顶点的入度变为 0,则将其加入队列。这表示在处理完当前级别后,这个顶点所代表的火车站也可以被确定为一个新的级别(在当前已处理的级别基础上)。

结果确定:重复上述步骤,直到队列为空。每一轮从队列中取出顶点并处理的过程,相当于确定了一个新的级别。最终统计处理的轮数,这个轮数就是 n 个火车站最少划分的级别数 。因为拓扑排序的特性保证了在处理过程中,先处理的顶点级别不高于后处理的顶点级别,通过这种方式能得到满足车次运行要求的最少级别划分。

建立边的及技巧

9 2

4 1 3 5 6

这里的1->3->5->6,可以如下优化

代码:

#include <bits/stdc++.h>

using namespace std;

const int N=2010;

unordered_map<int,vector<pair<int,int>>>e;

int n,m;

int in[N];

bool st[N];

int q[N];

int dis[N]={0};

int h=0,t=-1;

void toposort(){

    for(int i=1;i<=n;i++)

        if(!in[i])q[++t]=i;

    while(h<=t){

        int u=q[h++];

        for(auto ww:e[u]){

            int v=ww.first;

            if(--in[v]==0){

                q[++t]=v;

            }

        }

    }

}int main(){

    cin>>n>>m;

    for(int i=1;i<=m;i++){

        memset(st,0,sizeof st);

        int cnt;

        cin>>cnt;

        int start=n,end=1;

        for(int j=0;j<cnt;j++){

            int stp;

            scanf("%d",&stp);

            start=min(start,stp);

            end=max(end,stp);

            st[stp]=1;

        }

        int v=i+n;

        for(int i=start;i<=end;i++){

            if(st[i]){

                e[i].push_back({v,0});

                in[v]++;

            }

            else {

                e[v].push_back({i,1});

                in[i]++;

            }

        }

    }

    toposort();

    for(int i=1;i<=n;i++)dis[i]=1;

    for(int i=0;i<n+m;i++){

        int u=q[i];

        for(auto ww:e[u]){

            int v=ww.first,w=ww.second;

            dis[v]=max(dis[v],dis[u]+w);

        }

    }

    int res=0;

    for(int i=1;i<=n;i++)res=max(res,dis[i]);

    cout<<res;

}

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

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

相关文章

Gan网络公式了解

Gan网络 生成器和判别器是亦敌亦友的关系 对于生成模型&#xff0c;损失函数很难定义->所以我们可以将生成模型的输出交给判别模型进行处理&#xff0c;来分辨好坏。 生成器的损失是通过判别器的输出来计算的&#xff0c;而判别器的输出是一个概率值&#xff0c;我们可以通过…

解决linux mysql命令 bash: mysql: command not found 的方法

首先得知道mysql命令或mysqladmin命令的完整路径 比如mysql的路径是&#xff1a; /usr/local/mysql/bin/mysql&#xff0c;我们则可以这样执行命令&#xff1a; ln -s /usr/local/mysql/bin/mysql /usr/bin © 著作权归作者所有,转载或内容合作请联系作者 喜欢的朋友记得点…

微服务存在的问题及解决方案

微服务存在的问题及解决方案 1. 存在问题 1.1 接口拖慢 因为一个接口在并发时&#xff0c;正好执行时长又比较长&#xff0c;那么当前这个接口占用过多的 Tomcat 连接&#xff0c;导致其他接口无法即时获取到 Tomcat 连接来完成请求&#xff0c;导致接口拖慢&#xff0c;甚至…

【css酷炫效果】纯CSS实现立体纸张折叠动效

【css酷炫效果】纯CSS实现悬浮阴影扩散交互 缘创作背景html结构css样式完整代码基础版进阶版(3d 悬浮效果) 效果图 通过CSS box-shadow与transition属性实现悬浮阴影扩散交互&#xff0c;为元素添加细腻的悬浮反馈。 想直接拿走的老板&#xff0c;链接放在这里&#xff1a;htt…

案例5_1:单位数码管显示0

文章目录 文章介绍效果图仿真图5_1放置单位数码管 代码5_1.c 文章介绍 效果图 仿真图5_1 复制案例1_2的仿真图&#xff0c;在此基础上修改 注意&#xff1a;栅格大小需要缩小 放置单位数码管 代码5_1.c #include <reg52.h>#define uchar unsigned char #define uint un…

Linux centos7误删/boot拯救方法

1.进入救援模式 插入CentOS 7安装光盘&#xff0c;重启系统。在开机时按BIOS设置对应的按键&#xff08;通常是F2等&#xff09;&#xff0c;将启动顺序调整为CD - ROM优先。 系统从光盘启动后&#xff0c;选择“Troubleshooting”&#xff0c;然后选择“Rescue a CentOS s…

操作系统八股文整理(一)

操作系统八股文整理 一、进程和线程的区别二、进程与线程的切换过程一、进程切换进程切换的步骤&#xff1a; 二、线程切换线程切换的步骤&#xff1a; 三、进程切换与线程切换的对比四、上下文切换的优化 三、系统调用一、系统调用的触发二、从用户空间切换到内核空间三、执行…

20250317笔记本电脑在ubuntu22.04下使用acpi命令查看电池电量

20250317笔记本电脑在ubuntu22.04下使用acpi命令查看电池电量 2025/3/17 18:05 百度&#xff1a;ubuntu查看电池电量 百度为您找到以下结果 ubuntu查看电池电量 在Ubuntu操作系统中&#xff0c;查看电池电量通常可以通过命令行或者图形界面来完成。下面是一些常见的方法&…

蓝桥杯备考----模拟算法 phone number

嗯。这道题可以在两个和三个数字加-&#xff0c;我们只要随便输出一个奏行 那么&#xff01;我们规范一下&#xff0c;我们尽可能的只在两个数字之间加&#xff0c;但是如果一共奇数个的话&#xff0c;我们就让最后三个成一组&#xff0c;也就是说&#xff0c;我们用的是个小贪…

【数据分享】2000—2024年我国省市县三级逐月归一化植被指数(NDVI)数据(Shp/Excel格式)

之前我们分享过2000—2024年逐月归一化植被指数&#xff08;NDVI&#xff09;栅格数据&#xff08;可查看之前的文章获悉详情&#xff09;&#xff0c;该数据来源于NASA定期发布的MOD13A3数据集&#xff01;很多小伙伴拿到数据后反馈栅格数据不太方便使用&#xff0c;问我们能不…

蓝桥杯真题——洛谷 Day10 并查集(枚举)

目录 P8651 [蓝桥杯 2017 省 B] 日期问题 P8635 [蓝桥杯 2016 省 AB] 四平方和 P8651 [蓝桥杯 2017 省 B] 日期问题 思路&#xff1a; 使用scanf读入&#xff0c;枚举从1960到2059&#xff0c;若符合题目形式&#xff0c;加入答案&#xff0c; 从小到大输出&#xff1a;存入结…

Linux应用:程序运行

kill kill命令的这种用法是向指定的进程发送特定信号编号的信号。信号在操作系统中是一种软件中断机制&#xff0c;用于通知进程发生了某种特定事件或要求进程执行特定操作。​ kill - 信号编号 进程 ID 信号编号的含义&#xff1a;不同的信号编号代表不同的事件或操作。例如…

基于SpringBoot+Vue3实现的宠物领养管理平台功能一

一、前言介绍&#xff1a; 1.1 项目摘要 随着社会经济的发展和人们生活水平的提高&#xff0c;越来越多的人开始关注并参与到宠物领养中。宠物已经成为许多家庭的重要成员&#xff0c;人们对于宠物的关爱和照顾也日益增加。然而&#xff0c;传统的宠物领养流程存在诸多不便&a…

SpringCloud 学习笔记2(Nacos)

Nacos Nacos 下载 Nacos Server 下载 | Nacos 官网 下载、解压、打开文件&#xff1a; 更改 Nacos 的启动方式 Nacos 的启动模式默认是集群模式。在学习时需要把他改为单机模式。 把 cluster 改为 standalone&#xff0c;记得保存&#xff01; 启动startup.cmd Ubuntu 启动…

Blender-MCP服务源码4-初始化项目解读

Blender-MCP服务源码4-初始化项目解读 上篇文章针对Blender开发框架完成了一个基础模板的搭建&#xff0c;并在Blender中成功进行了运行&#xff0c;那这个初始化项目中是如何进行页面效果呈现的&#xff0c;尝试手动进行功能精简来拆解项目代码 1-核心知识点 1&#xff09;如…

基于eNSP的IPV4和IPV6企业网络规划

基于eNSP的IPV4和IPV6企业网络规划 前言网络拓扑设计功能设计技术详解一、网络设备基础配置二、虚拟局域网&#xff08;VLAN&#xff09;与广播域划分三、冗余协议与链路故障检测四、IP地址自动分配与DHCP相关配置五、动态路由与安全认证六、广域网互联及VPN实现七、网络地址转…

C#特性和反射

1。特性概念理解&#xff1f; 特性&#xff08;Attribute&#xff09;是用于在【运行时】传递程序中各种元素&#xff08;比如类、属性、方法、结构、枚举、组件等&#xff09;行为信息的声明性标签。您可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所…

mysql5.x和mysql8.x查看和设置隔离级别

MySQL的隔离级别 级别标志值描述读未提交READ-UNCOMMITTED0存在脏读、不可重复读、幻读的问题读已提交READ-COMMITTED1解决脏读的问题&#xff0c;存在不可重复读、幻读的问题可重复读REPEATABLE-READ2mysql 默认级别&#xff0c;解决脏读、不可重复读的问题&#xff0c;存在幻…

3.17学习总结

写了两道题 刚开始用的之前做组合输出的方法&#xff0c;时间超限了&#xff0c;想不出怎么优化&#xff0c;后面看了题解&#xff0c;代码如下 #include <stdio.h> #include <stdlib.h> int n,min2e9; int a[11],b[11]; //搜索 void hly(int s,int x,int y) {//当…

Blender材质 - 层权重

层权重 混合着色器 可以让 面朝向的一面显示一种材质 另一面显示另一种材质 就能实现挺不错的材质效果 移动视角 材质会跟着变化 有点类似虚幻的视差节点BumpOffset