拓扑排序(初中组)

news2024/11/17 13:28:50

有向无环图

定义

边有向,无环。英文名叫 Directed Acyclic Graph,缩写是 DAG。一些实际问题中的二元关系都可使用 DAG 来建模。

性质

  • 能 拓扑排序 的图,一定是有向无环图;

    如果有环,那么环上的任意两个节点在任意序列中都不满足条件了。

  • 有向无环图,一定能拓扑排序;

    (归纳法)假设节点数不超过 kk 的 有向无环图都能拓扑排序,那么对于节点数等于 kk 的,考虑执行拓扑排序第一步之后的情形即可。

判定

如何判定一个图是否是有向无环图呢?

检验它是否可以进行 拓扑排序 即可。

当然也有另外的方法,可以对图进行一遍 DFS ,在得到的 DFS 树上看看有没有连向祖先的非树边(返祖边)。如果有的话,那就有环了。

拓扑排序

定义

拓扑排序的英文名是 Topological sorting。

拓扑排序要解决的问题是 给一个图的所有节点排序

我们可以以游戏闯关为例。以下图为例,我们初始只能从第一关开始打,当第 11 关打完后,可以依次打第 2,3,42,3,4 关,当第 44 关打完后,第 5∼95∼9 关都可以打,以此类推,某个关卡打完之后,其相邻的关卡就可以打了,否则不能打。现在让你安排一个合理的闯关顺序,来将所有的关卡都完成。

 

上面这个游戏闯关图,我们可以将其抽象为一个以关卡 11 为根的一棵树,或者说是有向无环图,边的方向为从编号小的节点走向编号大的节点。我们可以直接按 1∼321∼32 的顺序依次闯关,或者其他合法的顺序闯关。但是如果我们想把第 11 关闯完后,就直接闯第 2828 关,就不可以。因为闯第 2828 关之前,必须保证第 1313 关先闯完了,才可以。

一个合法的满足规则的闯关顺序就是一个拓扑排序。

实际上的拓扑排序是在更复杂的图上进行的,以下图为例:

对其排序的一种合法的结果就是:2→8→0→3→7→1→5→6→9→4→11→10→122→8→0→3→7→1→5→6→9→4→11→10→12。

但是如果图上出现了一个环,那么,我们就无法对图进行拓扑排序了。这也就是上面我们说的结论:只有有向无环图(DAG)上才可以进行拓扑排序。

因此我们可以说: 在一个 DAG(有向无环图) 中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 uu 到 vv 的有向边 (u,v)(u,v) , 都可以有 uu 在 vv 的前面。

还有给定一个 DAG,如果从 ii 到 jj 有边,则认为 jj 依赖于 ii 。如果 ii 到 jj 有路径( ii 可达 jj ),则称 jj 间接依赖于 ii 。

拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。

实现拓扑排序可以用 Kahn 算法和 DFS 算法实现,下面逐一介绍。

Kahn 算法

基本原理

Kahn 算法基于以下思想实现:

  1. 一条入边即为一条依赖,有多少入度,就有多少个依赖。
  2. 对于所有未访问的节点,我们必须从入度为 00 的节点开始访问。
  3. 访问一个节点后,其节点的出边所造成的依赖消失,该出边对应的入度就减少。

因此,Kahn 算法实现步骤如下:

  1. 初始状态下,集合 SS 装着所有入度为 00 的点, LL 是一个空列表。

  2. 每次从 SS 中取出一个点 uu (可以随便取)放入 LL , 然后将 uu 的所有边 (u,v1),(u,v2),(u,v3)⋯(u,v1​),(u,v2​),(u,v3​)⋯ 删除。对于边 (u,v)(u,v) ,若将该边删除后点 vv 的入度变为 00 ,则将 vv 放入 SS 中。

  3. 不断重复以上过程,直到集合 SS 为空。

  4. 检查图中是否存在任何边或者还有节点没有被访问过,如果有,那么这个图一定至少存在一条环路,否则返回 LL , LL 中顶点的顺序就是拓扑排序的结果。

模板题:P2243 - 拓扑排序

示例代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e3+5;
int n, m, x, y;
vector<int> g[N];
vector<int> ans;
int cnt[N], tpc;

// 判断有无环 
bool kahn()
{
    queue<int> q;
    for(int i = 1; i <= n; i++) {  // 将入度为 0 的点入队
        if(!cnt[i])
            q.push(i);
    }   
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        tpc++;   // 统计访问过的节点数量
        ans.push_back(u);  // 压入已访问节点列表
        for(auto v : g[u])
        {
            cnt[v]--;
            if(!cnt[v])  // 入度为 0, 没有任何依赖,可以访问
                q.push(v);
        }
    }
    return tpc == n;  // 如果所有节点都访问了,说明不存在环,否则有环
}

int main()
{
    cin >> n >> m;
    while(m--)
    {
        cin >> x >> y;
        g[x].push_back(y);
        cnt[y]++;  // 统计各个节点的入度
    }
    if(!kahn())
        cout << "loop exist.";
    else
    {
        cout << "loop not exist." << endl;
//        for(int i = 0; i < ans.size(); i++)
//            cout << ans[i] << " ";
        for(auto x : ans) cout << x << " ";
    }
    return 0;
}

Copy

代码的核心是维持一个入度为 00 的顶点的集合,这里使用了队列,当然,换成栈或者优先队列等都可以。这样,使用不同的数据结构维持入度为 00 的顶点的集合,其最终的访问的顺序也不尽相同。

所以我们从代码也可以看出: 对于一个 DAG 图,拓扑排序的结果可能不唯一

时间复杂度

假设这个图 G=(V,E)G=(V,E) 在初始化入度为 00 的集合 SS 的时候就需要遍历整个图,并检查每一条边,因而有 O(E+V)O(E+V) 的复杂度。然后对该集合进行操作,显然也是需要 O(E+V)O(E+V) 的时间复杂度。

因而总的时间复杂度就有 O(E+V)O(E+V) 。

DFS 算法

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5+5;
int n, m, x, y;
vector<int> G[N];  // vector 实现的邻接表
int c[N];      // 标志数组  -1:访问过但仍有出度,0: 未访问,1: 出度为 0 的节点
vector<int> topo;     // 拓扑排序后的节点

bool dfs(int u) {
    
    c[u] = -1;
    for (int v : G[u]) {
        if (c[v] < 0)  return false;  // 节点被访问过了,有返祖边,有环
        // 节点出度不为 0,但是从该节点出发有返祖边,有环
        if (!c[v] && !dfs(v)) return false;
    }
    c[u] = 1;  // 节点 u 的所有出边已访问完,出度为 0 了,入拓扑节点列表
    topo.push_back(u);
    return true;
}

bool toposort() {
    topo.clear();
    memset(c, 0, sizeof(c));
    for (int u = 1; u <= n; u++) {
        if (!c[u]&&!dfs(u))
            return false;
    }
    reverse(topo.begin(), topo.end());
    return true;
}

int main()
{
    cin >> n >> m;
    while(m--)
    {
        cin >> x >> y;
        G[x].push_back(y);
    }
    if(!toposort())
        cout << "loop exist.";
    else
    {
        cout << "loop not exist." << endl;
        for(auto x : topo)
            cout << x << " ";
    }
    return 0;
}

 

时间复杂度: O(E+V)O(E+V)

空间复杂度: O(V)O(V)

合理性证明

这个算法的实现非常简单,但是要理解的话就相对复杂一点。

关键在于为什么在 dfs 方法的最后将该顶点添加到一个集合中,就能保证这个集合就是拓扑排序的结果呢?

因为添加顶点到集合中的时机是在 dfs 方法即将退出之时,而 dfs 方法本身是个递归方法,只要当前顶点还存在边指向其它任何顶点,它就会递归调用 dfs 方法,而不会退出。因此,退出 dfs 方法,意味着当前顶点没有指向其它顶点的边了,即当前顶点是一条路径上的最后一个顶点。

两种方法的对比

这两种算法分别使用队列和栈来表示结果集。

对于基于DFS的算法,加入结果集的条件是:顶点的出度为 00。这个条件和 Kahn 算法中入度为 00 的顶点集合似乎有着异曲同工之妙,这两种算法的思想犹如一枚硬币的两面,看似矛盾,实则不然。一个是从入度的角度来构造结果集,另一个则是从出度的角度来构造。

二者的复杂度均为 O(V+E)O(V+E)。

应用与问题变形

判环

拓扑排序可以用来判断图中是否有环,还可以用来判断图是否是一条链。

字典序最大/最小的拓扑排序

对于 Kahn 算法,我们可以将 Kahn 算法中的队列替换成最大堆/最小堆实现的优先队列,这样就可以求出字典序最大/最小的拓扑排序,此时总的时间复杂度为 O(E+Vlog⁡V)O(E+VlogV) 。

拓扑排序解的唯一性

哈密顿路径:哈密顿路径是指一条能够对图中所有顶点正好访问一次的路径。

接下来我们只会解释一些哈密顿路径和拓扑排序的关系,至于哈密顿路径的具体定义以及应用,可以参见其他文章或自行搜索了解。

前面说过,当一个 DAG 中的任何两个顶点之间都存在可以确定的先后关系时,对该 DAG 进行拓扑排序的解是唯一的。这是因为它们形成了全序的关系,而对存在全序关系的结构进行线性化之后的结果必然是唯一的(比如对一批整数使用稳定的排序算法进行排序的结果必然就是唯一的)。

需要注意的是, 非 DAG 也是能够含有哈密顿路径的,但是为了利用拓扑排序来实现判断,这里讨论的主要是判断 DAG 中是否含有哈密顿路径的算法。

我们先使用拓扑排序对图中的顶点进行排序。排序后,对每对相邻顶点进行检测,看看是否存在先后关系,如果每对相邻顶点都存在着一致的先后关系(在有向图中,这种先后关系以有向边的形式体现,即查看相邻顶点对之间是否存在有向边)。那么就可以确定该图中存在哈密顿路径了,反之则不存在。

习题

  • 拓扑排序 - TopsCoding
  • 可达性统计 - TopsCoding
  • 家谱树 - TopsCoding
  • 奖学金 - TopsCoding
  • 朋友排队 - TopsCoding
  • 最长路 - TopsCoding
  • 旅行计划 - TopsCoding
  • NOIP2003 提高组 神经网络 - TopsCoding
  • 车站分级 - TopsCoding
  • HAOI2016 食物链 - TopsCoding
  • 最大食物链计数 - TopsCoding
  • USACO 4.4.3 Frame Up 重叠的图像 - TopsCoding
  • HNOI2015 菜肴制作 - TopsCoding

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

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

相关文章

特征构造和降维

特征构造&#xff08;Feature Engineering&#xff09; 特征构造是从现有数据中创建新的特征&#xff0c;以揭示数据中的隐藏关系&#xff0c;从而提高模型表现。这是数据预处理中一个关键步骤&#xff0c;可以显著提升模型的性能。 原理 通过特征构造&#xff0c;我们可以利…

“云计算环境下的等保测评要点分析“

随着云计算技术的广泛应用&#xff0c;企业越来越多地将业务迁移到云端&#xff0c;这不仅带来了灵活性和效率的提升&#xff0c;也对信息安全提出了新的挑战。等保测评作为我国信息安全等级保护制度的重要组成部分&#xff0c;其在云计算环境下的实施具有特殊性。本文将围绕“…

nginx 简单使用方法

nginx是用于 Web 服务、反向代理、内容缓存、负载均衡、媒体流传输等场景的开源软件。 主要作用有三个&#xff1a;1、反向代理 负载均衡动静分离 下载地址&#xff1a;nginx: download nginx执行命令及启动 //假设安装在E:\server\nginx-1.20.0目录下 //cmd命令进入安装文…

如何在 VitePress 中增加一个全局自定义组件

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storm…

【Python实战】轻松实现自动发送邮件

本文收录于 《一起学Python趣味编程》专栏,从零基础开始,分享一些Python编程知识,欢迎关注,谢谢! 文章目录 一、前言二、开发准备三、正式开始四、总结一、前言 本文介绍如何使用Python开源项目“PythonSendMail”,快速实现自动发送带Excel报表附件的邮件。只需要进行简…

操作系统篇--八股文学习第十天| 进程和线程之间有什么区别;并行和并发有什么区别;解释一下用户态和核心态,什么场景下,会发生内核态和用户态的切换?

进程和线程之间有什么区别 答&#xff1a; 进程是资源调度和分配的基本单位。 线程是程序执行的最小单位&#xff0c;线程是进程的子任务&#xff0c;是进程内的执行单元。 一个进程至少有一个线程&#xff0c;一个进程可以运行多个线程&#xff0c;这些线程共享同一块内存…

oracle rac-> rac配置adg避坑注意点

例如源主库db_name为aaa db_unique_name为aaa&#xff0c;实例名为aaa1,aaa2 ORACLE_SID为aaa1,aaa2 tnsnames.ora的服务名配置&#xff1a;aaa/dgaaa 则备库设置参考&#xff1a;db_name相同为aaa&#xff0c;db_unique_name不同为dgaaa&#xff0c;实例名为dgaaa1,dgaaa2…

【LeetCode】56. 区间合并

区间合并 题目描述&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 示例 1&#xff1a; …

Oracle的一些脚本工具总结

一、操作系统性能 在linux 6 之后&#xff0c;建议使用dstat监控操作系统的各项指标。 使用方法&#xff1a; yum install dstat dstat -cdlrgmnpsy 二、we.sql https://download.csdn.net/download/zengxiangbo/89601165 详见文章关联附件。 Oracle脚本工具&#xff0c;查…

企业安全生产管理是否将成为新的朝阳产业?

这个答案我可以肯定的告诉你&#xff0c;是的&#xff01; 首先朝阳产业是啥&#xff1f;朝阳产业指的是具有广阔发展前景、高成长性和创新性的产业。 但你也有可能不信我说的这句。因为在大多数人看来&#xff0c;安全生产管理这块儿&#xff0c;企业的关注度不多&#xff0…

Oracle Database 确认表空间大小的方法

Oracle Database 确认表空间大小的方法 sql: WITH object_size AS (SELECT segment_name,owner,segment_type,tablespace_name,SUM(bytes) total_bytesFROM dba_segmentsGROUP BY segment_name, owner, segment_type, tablespace_name ), table_size AS (SELECT segment_name …

【探索Linux】P.43(网络层 —— IP协议)

阅读导航 引言一、IP协议基本概念二、IP协议头格式三、网段划分1. 网络号和主机号2. 引入子网掩码&#xff08;1&#xff09;基本概念&#xff08;2&#xff09;默认子网掩码&#xff08;3&#xff09;子网掩码的作用&#xff08;4&#xff09;子网掩码与IP地址的结合&#xff…

探索Python代码质量的守护神:Flake8

文章目录 探索Python代码质量的守护神&#xff1a;Flake8背景&#xff1a;为何选择Flake8&#xff1f;简介&#xff1a;Flake8是什么&#xff1f;安装&#xff1a;如何获取Flake8&#xff1f;使用方法&#xff1a;Flake8的基本命令场景应用&#xff1a;Flake8在实际开发中的作用…

SOMEIPSRV_RPC_13: 不同服务可以共享相同的端口

测试目的&#xff1a; 验证不同的服务是否能够共享同一个传输层协议的端口号。 描述 本测试用例旨在验证在同一个电子控制单元(ECU)上&#xff0c;不同服务的多个服务实例是否能够监听不同的端口&#xff0c;同时确保不同的服务能够共享同一个端口号。 测试拓扑&#xff1a…

接口测试:使用 curl 发送请求

简介 cURL 是一个通过 URL 传输数据的&#xff0c;功能强大的命令行工具。cURL 可以与 Chrome Devtool 工具配合使用&#xff0c;把浏览器发送的真实请求还原出来&#xff0c;附带认证信息&#xff0c;脱离浏览器执行&#xff0c;方便开发者重放请求、修改参数调试&#xff0c;…

SAP HCM 薪酬过账-忽略成本分配的含义

导读 INTRODUCTION 忽略成本分配&#xff1a;昨天分析C0表与C1表的作用&#xff0c;是为今天说的忽略成本分配按钮的铺垫&#xff0c;忽略成本分配这个翻译有点难让人理解&#xff0c;从字母意思感觉就是所有成本类的不拆分处相关的行项目&#xff0c;但是实际不是这样&#xf…

大数据-56 Kafka SpringBoot与Kafka 基础简单配置和使用

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

ALB快速实现IPv4服务的负载均衡

阿里云应用型负载均衡ALB支持HTTP、HTTPS和QUIC协议&#xff0c;专门面向网络应用层&#xff0c;提供强大的业务处理能力。 为了实现IPv4服务的负载均衡&#xff0c;需要快速创建一个ALB实例&#xff0c;并将来自客户端的访问请求转发至后端服务器。 操作流程 第一步&#x…

多语言 AI 翻译 API 数据接口

AI / 翻译 基于 AI 多语言模型 支持多语言 / 基于模型。 1. 产品功能 基于自有专业模型进行 AI 多语言翻译高效的文本翻译性能全接口支持 HTTPS&#xff08;TLS v1.0 / v1.1 / v1.2 / v1.3&#xff09;&#xff1b;全面兼容 Apple ATS&#xff1b;全国多节点 CDN 部署&#xf…

ITSS服务经理/ITSS服务工程师,招投标需要准备吗?

信息技术服务标准&#xff08;ITSS&#xff09;是中国首套完整的信息技术服务标准体系&#xff0c;全面规定了IT服务产品及其组成要素的标准化实施&#xff0c;旨在提供可信赖的IT服务。 在国际竞争日益激烈的背景下&#xff0c;推动国内标准的国际化已成为广泛共识&#xff0…