数据结构:图的拓扑排序与关键路径

目录

一、拓扑排序

1.1、算法的基本步骤

1.2、算法实现

1.3、习题思考

1.4、DFS生成逆拓扑序

二、关键路径

1.1、关键路径的原理

1.2、算法的基本步骤

1.3、算法实现

1.4、习题思考


一、拓扑排序

43dff1fbc5604feeaecc9992b4c901c4.png

         AOV网:有向图中, 顶点表示活动(或任务), 有向边表示活动(或任务)间的先后关系,称这样的有向图为AOV网(Activity On Vertex Network)。
        AOV网络中不能出现有向回路,即有向环。
          拓扑序列:就是把AOV网中的所有顶点排成一个线性序列,若AOV网中存在有向边 Vi Vj ,则在该序列中, Vi 必位于 Vj 之前。在拓扑序列中,先进行的任务一定在后进行的任务的前面。按照拓扑序列完成各子任务,就可以顺利完成整个任务。拓扑序列未必唯一。
如果不能排成一个拓扑序列,则说明AOV网络中存在有向环。

1.1、算法的基本步骤

①从图中选择一个入度为0的顶点并输出。

②从图中删除该顶点及该顶点引出的所有边。

③执行①②,直至所有顶点已输出,或剩余顶点入度均不为0(说明存在环,无法继续拓扑排序)

时间复杂度O(n+e):计算入度O(n+e),删除顶点O(n+e)。

1.2、算法实现

        实际上我们并不需要真正的删除该顶点,我们只需要记录每个顶点的入度,在“删除”某个顶点之后,它所邻接到的所有顶点的入度都减1即可。并且我们用栈保存入度为0的顶点,以供删除。课本上用数组实现栈,这里直接采用STL:stack。

#include<bits/stdc++.h>
using namespace std;
#define n 10
struct Edge{int VerName;int cost;Edge * next;
};
struct Vertex{int VerName;Edge * edge=nullptr;
};
vector<int> cnt(n);
Vertex Head[n];
vector<int> path;
void TopoOrder(){//进行拓扑排序,并判断是否存在环路path.clear();cnt.assign(n,0);for(auto & i : Head){//计算入度for(Edge * edge=i.edge;edge!=nullptr;edge=edge->next){cnt[edge->VerName]+=1;//入度++}}stack<int> sta;for(int i=0;i<n;++i) if(!cnt[i]) sta.push(i);//初始化入度为0的家伙int num=0;while(!sta.empty()){//拓扑排序int cur=sta.top();sta.pop();path.emplace_back(cur);//路径存入for(Edge * edge=Head[cur].edge;edge!=nullptr;edge=edge->next){//删除点cnt[edge->VerName]-=1;//入度++if(cnt[edge->VerName]==0) sta.push(edge->VerName);}}if(path.size()!=n) printf("有向图中存在环路");else for(auto i:path) printf("%d ",i);return;
}int main(void){return 0;
}

        由于一个顶点的入度被删减为0时,不可能再有边指向该顶点,因此该顶点不会被再次访问,则其cnt[i]空闲了。即当cnt[i]==0时,cnt[i]就不会被访问了,i也会入栈,因此我们可以利用这个空闲直接在cnt上实现栈。

优化用cnt数组空闲空间实现栈(静态链表):

int top=-1;//模拟栈顶
for(int i=0;i<n;++i){if(cnt[i]==0){cnt[i]=top;//指向以前的栈顶top=i;//栈顶变为i}
}while(top!=-1){int cur=top;path.emplace_back(cur);top=cnt[top];//静态链表,相当于弹栈for(Edge * edge=Head[cur].edge;edge!=nullptr;edge=edge->next){//删除点cnt[edge->VerName]-=1;//入度++if(cnt[edge->VerName]==0) {cnt[edge->VerName]=top;top=edge->VerName;}}
}

1.3、习题思考

(1)给定一个图和顶点序列,编写算法判断该序列是否是图的拓扑序列。

只需要先计算一次入度,然后扫描所给定的顶点序列,对于每一个顶点:

①先判断该顶点入度是否为0,如果不为0,则说明不是拓扑序,如果为0,则进行②

②将该顶点的边删除(即为其邻接顶点减少入度),之后继续扫描。

        实际上我们假定给定的顶点序列是拓扑序列,则按照该顶点序列选择顶点删除边,选择的顶点一定是入度为0的。

(2)802. 找到最终的安全状态

60caf4bc64614950b536e8b55f93d644.png

        如果本题正着想,则从一个点开始DFS遍历,如果它能遍历到终端结点,则路径上的所有点都能遍历到终端结点,如果它的所有邻接结点都是安全结点,则它也将是安全结点,如果它有一个邻接结点不是安全结点则必然它也不是安全结点(换句话说如果存在环路,则遍历到一个结点不是安全结点且已经被遍历过,则这条路并不通往终端节点,则它不是安全结点)。然后继续寻找下一个不是安全结点的结点且未被遍历过的结点开始遍历。<不是安全结点的结点且被遍历过的结点一定不可能是安全结点了,因为如果它是,那么它的所有邻接结点都是安全结点。后根DFS也将判断它为安全结点。>


        如果说你能走到终端结点,你才算是安全结点。那么反过来走,只能被终端节点才能走到的结点才是安全结点。考察反向图,终端节点是入度为0的顶点,进行拓扑排序,排序过程中如果入度为0则被认为是安全结点(证明:开始时终端结点为安全结点,入度为0,删除其所有出边,剩下的结点入度为0的则说明它们只能被终端结点走到,它们是安全结点,然后把它们当作终端结点继续执行同样的操作,以此类推)。

class Solution {
public:vector<int> eventualSafeNodes(vector<vector<int>>& graph) {int n=graph.size();vector<vector<int>> gra(n,vector<int>{});vector<int> cnt(n);stack<int> sta;vector<int> ans;for(int i=0;i<n;++i){cnt.at(i)=graph.at(i).size();if(!cnt.at(i)){sta.push(i);}for(int j=0;j<cnt.at(i);++j){gra.at(graph.at(i).at(j)).emplace_back(i);}}while(!sta.empty()){ans.push_back(sta.top());sta.pop();for(auto i:gra[ans.back()]){cnt.at(i)--;if(cnt.at(i)==0) sta.push(i);}}sort(ans.begin(),ans.end());return ans;}
};
//什么STL大王,在这全程at(),看得脑袋晕,一般还是别用了(。。),用起来自己看不明白呀,除非你想测试越界

1.4、DFS生成逆拓扑序

为什么DFS能生成逆拓扑序?

        我们来思考一下拓扑序列的原始定义。在一个拓扑序列中,如果vi在vj之前,则必然在有向图中有vi→vj。那么如果我们使用DFS“后根遍历”,则对于任意的vi→vj,一定有vi在vj之后输出,换句话说,对于有向图中的每一条边vi→vj均满足,vi在vj之后,如果将这一输出序列反转,则等价于对于有向图中的每一条边vi→vj均满足,vi在vj之前。相当于对于任何一个顶点vi,其后继邻接顶点都会在其之前被输出,否则它不会被输出。反过来就是,只有当vi被输出之后,其邻接顶点才会被输出,即满足拓扑排序。因此DFS“后根输出”为拓扑排序的逆序。

        当然你得先保证这是一个AOV网络,不然DFS就变成了纯遍历了。

int vis[n];
void DFS_TopoOrder(int root){vis[root]=1;for(Edge * edge=Head[root].edge;edge!=nullptr;edge=edge->next){if(vis[root]==0)DFS_TopoOrder(edge->VerName);}printf("%d ",root);return;
}
for(int i=0;i<n;++i)if(cnt[i]==0) DFS_TopoOrder(i);//从入度为0的开始遍历

二、关键路径

AOV网(Activity On Vertex):顶点表示活动或任务(Activity), 有向边表示活动(或任务) 间的先后关系。
➢AOE网(Activity On Edges):有向边表示活动或任务(Activity) , 用边上的权值表示活动的持续时间,顶点称为事件(Event): 表示其入边的任务已完成,出边的任务可开始的状态。
        关键路径的问题的图是只有一个源点和一个汇点的有向无环图。每一个事件,最早开始的时间取决于其前驱活动最晚完成的时间,因为只有在所有前驱活动完成了它才可以开始(这和拓扑排序很像)。 
        完成整个工程(所有活动均被完成)所需的最短时间取决于从源点到汇点的最长路径长度,关键路径就是这条路径长度最长的路径,关键路径上的活动被称为关键活动。

1.1、关键路径的原理

(1)关键路径为什么是最长路径?

        反证法证明:由于一个工程完成需要所有活动都结束。从源点出发,任意的一条道路中,走最长路径的那条路是花费时间最多的,因此工程完成至少要这条路完成。但是会不会存在一种情况使得这条路径上的某个事件被其他路径上的活动影响到,搁置了这条路的时间呢(也就是说完成整个工程的时间并不是关键路径)?我们假定存在这种情况:设这个被搁置的事件为event,该事件是关键路径上的一个事件,我们知道从源点到汇点的最长路径是 从源点经过event,再从event到汇点的。如果存在一个不是关键路径上的活动到event,关键路径上从源点到event的时间搁置了,导致完成整个工程所需的时间更长了,但是event到汇点的时间没变。由于关键路径是从源点到汇点长度最长的路径,因此关键路径应该要换成那个使得从源点到event时间更长的路径,而不是现在所说的关键路径,因此与假设的关键路径矛盾。

(2)为什么关键活动是最早开始时间和最迟开始时间相等的?

        我们知道关键路径上的活动决定了整个工程完成的时间,因此如果让汇点的最早开始时间等于最迟开始时间的话,关键路径上的活动开始时间都不能推迟和提早,因此它们的最早开始时间也和最迟开始时间相同,不能被延误,如果被延误的话则汇点的开始时间也会被延误。当然我们没必要一定求出关键活动,求出“关键事件”是一样的。

(3)事件的最早开始时间应该怎么计算?

        只有当一个事件的前驱事件全都完成了,它才可以开始,因此最早开始时间的计算方式是:拓扑排序指明了事件开始的必然先后顺序。

(4)事件的最迟开始时间应该怎么计算?

        一个事件最迟开始的时间,就是它在不影响整个工程的时间的情况下,可以一直拖到开始的时间,因为最迟开始要让它不影响 后面事件的 最迟开始的时间,因此它是在尽可能迟的情况下尽可能不耽误,因此是取最迟里面的最小值。

1.2、算法的基本步骤

        如果只需要求出从源点到汇点的最长路径的长度,只需要使用拓扑排序+动态规划就可以求出来。定义dis[i]表示从源点到顶点i的最长路径,则dis[源]=0,dis[i]=max(dis[i],dis[u]+weight(u,i));

        但如果需要求出关键活动则必须求出最早发生时间和最迟发生时间相等的事件,因此需要再利用一次逆拓扑排序+动态规划求出。定义dis2[i]表示从顶点i到汇点的最长路径,则dis2[汇]=max_len,dis2[i]=min(dis2[i],dis[u]-weight(i,u))。

        时间复杂度:O(n+e),拓扑排序O(n+e),动态规划O(n)

①求出图的拓扑序列,如果图中存在环则退出
②求出每个事件的最早开始时间(求出每个顶点从源点出发到该点的最长路径)
③求出每个事件的最迟开始时间(实际上是反着求最长路径)
④如果最早开始时间和最迟开始时间相等,则该点为关键活动对应的事件。

1.3、算法实现

#include<bits/stdc++.h>
using namespace std;
#define n 10
struct Edge{int VerName;int cost;Edge * next;
};
struct Vertex{int VerName;Edge * edge=nullptr;
};
vector<int> cnt(n);
Vertex Head[n];
bool TopoOrder(Vertex Head[],vector<int>& path) {path.clear();cnt.assign(n,0);for(int i=0;i<n;++i){//计算入度for(Edge * edge=Head[i].edge;edge!=nullptr;edge=edge->next){cnt[edge->VerName]+=1;//入度++}}stack<int> sta;for(int i=0;i<n;++i) if(!cnt[i]) sta.push(i);//初始化入度为0的家伙int num=0;while(!sta.empty()){//拓扑排序int cur=sta.top();sta.pop();path.emplace_back(cur);//路径存入++num;for(Edge * edge=Head[cur].edge;edge!=nullptr;edge=edge->next){//删除点cnt[edge->VerName]-=1;//入度++if(cnt[edge->VerName]==0) sta.push(edge->VerName);}}if(num!=n) {printf("有向图中存在环路");return false;}return true;;
}
vector<int > v_latest(n,0x3f3f3f3f);
vector<int > v_earliest(n);
void CriticalPath(Vertex Head[]) {vector<int> path;if(!TopoOrder(Head,path)) return;v_earliest.at(path.front())=0;for(auto i:path) {//最早开始时间for(Edge * edge=Head[i].edge;edge!=nullptr;edge=edge->next) {int k=edge->VerName;v_earliest[k]=max(v_earliest[k],v_earliest[i]+edge->cost);}}v_latest.at(path.back())=v_earliest.at(path.back());for(auto i=path.rbegin();i!=path.rend();++i) {//最迟开始时间for(Edge * edge=Head[*i].edge;edge!=nullptr;edge=edge->next) {v_latest[*i]=min(v_latest[*i],v_latest[edge->VerName]-edge->cost);}}printf("关键路径为:");for(auto i:path) {if(v_earliest[i]!=v_latest[i]) continue;for(Edge * edge=Head[i].edge;edge!=nullptr;edge=edge->next) {if(v_earliest[edge->VerName]==v_latest[edge->VerName]) {printf("%d->%d\n",i,edge->VerName);break;}}}return;
}
int main(void) {CriticalPath(Head);return 0;
}

1.4、习题思考

a.下图是有 10 个活动的 AOE 网,其中时间余量最大的活动是 ______
需要求活动的最早开始时间和最迟开始时间,它取决于它弧尾的最早开始时间,和弧头的最晚开始时间。

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

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

相关文章

Android ViewPager不支持wrap_content的原因

文章目录 Android ViewPager不支持wrap_content的原因问题源码分析解决 Android ViewPager不支持wrap_content的原因 问题 <androidx.viewpager.widget.ViewPagerandroid:id"id/wrap_view_pager"android:layout_width"match_parent"android:layout_he…

【氮化镓】利用Ga2O3缓冲层改善SiC衬底AlN/GaN/AlGaN HEMT器件性能

Micro and Nanostructures 189 (2024) 207815文献于阅读总结。 本文是关于使用SiC衬底AlN/GaN/AlGaN高电子迁移率晶体管&#xff08;HEMT&#xff09;的研究&#xff0c;特别是探讨了不同缓冲层对器件性能的影响&#xff0c;以应用于高速射频&#xff08;RF&#xff09;应用。…

ClickHouse01-什么是ClickHouse

什么是ClickHouse&#xff1f; 关于发展历史存在的优势与劣势什么是它风靡的原因&#xff1f; 什么是ClickHouse&#xff1f; 官方给出的回答是&#xff0c;它是一个高性能、列式存储、基于SQL、供在线分析处理的数据库管理系统 当然这边不得不提到OLAP(Online Analytical Pr…

信息学奥赛之MAC端VSCode C++环境配置

前提 安装 Visual Studio CodeVSCode 中安装 C/C扩展确保 Clang 已经安装&#xff08;在终端中输入命令&#xff1a;clang --version 来确认是否安装&#xff09;未安装&#xff0c;在命令行执行xcode-select --install 命令&#xff0c;会自行安装&#xff0c;安装文件有点大…

ideaSSM博物馆网站系统VS开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 idea 开发 SSM 博物馆网站系统是一套完善的信息管理系统&#xff0c;结合SSM框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff0c…

爬虫分析-基于Python的空气质量数据分析与实践

概要 本篇文章利用了Python爬虫技术对空气质量网站的数据进行获取&#xff0c;获取之后把数据生成CSV格式的文件&#xff0c;然后再存入数据库方便保存。再从之前24小时的AQI&#xff08;空气质量指数&#xff09;的平均值中进行分析,把数据取出来后&#xff0c;对数据进行数据…

第12篇:4线-2线普通编码器

Q&#xff1a;在数字系统中&#xff0c;用一个二进制代码表示特定信息称为编码&#xff0c;而具有编码功能的逻辑电路就称为编码器。本期我们学习实现4线-2线普通编码器。 A&#xff1a;基本原理&#xff1a;二进制编码器有n位输出&#xff0c;与个输入相对应。4线-2线普通编码…

mysql 如何设计分库分表

在MySQL中设计分库分表的方法通常涉及到水平拆分与垂直拆分两种主要方式。 水平拆分&#xff1a; 按照某一列进行水平拆分&#xff1a; 可以根据某一列&#xff08;如用户ID、时间等&#xff09;的取值范围将数据拆分到不同的数据库或表中。基于哈希值的水平拆分&#xff1a;…

银行OA系统|基于SpringBoot架构+ Mysql+Java+ B/S结构的银行OA系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

Orange3数据预处理(行选择组件)

选择行 根据数据特征的条件选择数据实例。 输入 数据&#xff1a;输入数据集 输出 匹配数据&#xff1a;满足条件的实例 不匹配数据&#xff1a;不满足条件的实例 数据&#xff1a;带有额外列的数据&#xff0c;显示实例是否被选中 这个小部件根据用户…

如何使用Excel创建一个行政考勤表

在企业和机构中&#xff0c;行政考勤管理是一项重要的日常工作&#xff0c;它涉及到员工的出勤情况、请假记录、加班情况等。使用Excel创建一个行政考勤表可以帮助管理者有效地记录和跟踪员工的考勤情况&#xff0c;下面将详细介绍如何使用Excel创建一个行政考勤表。 第一部分&…

SpringMVC | SpringMVC中的 “JSON数据交互“ 和 “RESTful设计风格“

目录: 一、JSON 数据交互1.1 JSON概述1.2 JSON的“数据结构”对象结构数组结构 1.3 JSON的“数据转换”用 \<mvc:annotation-driven/>的方式 来“自动配置” MappingJackson2HttpMessageConverter转换器 (来完成JSON数据转换)用\<bean>标签方式的来“自行配置” JS…

【软考】生成树

目录 1. 概念2. 图解3. 例题3.1 例题1 1. 概念 1.对于有n个顶点的连通图&#xff0c;至少有n-1条边&#xff0c;而生成树中恰好有n-1条边2.连通图的生成树是该图的极小连通子图3.若在图的生成树中任意加一条边&#xff0c;则必然形成回路4.图的生成树不是唯一的5.从不同的顶点…

如何在Linux系统部署Dupal CMS结合内网穿透实现无公网IP访问web界面

文章目录 前言1. Docker安装Drupal2. 本地局域网访问3 . Linux 安装cpolar4. 配置Drupal公网访问地址5. 公网远程访问Drupal6. 固定Drupal 公网地址 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&a…

【OpenCV C++Python】(五)图像平滑(模糊)

文章目录 图像平滑均值滤波高斯滤波中值滤波双边滤波(Bilateral Filtering ) PythonC 图像线性平滑空间滤波&#xff08;加权均值滤波器&#xff0c;几何均值滤波&#xff0c;谐波均值滤波&#xff0c;逆谐波均值滤波&#xff09;&#xff0c;非线性平滑空间滤波&#xff08;中…

Linux下QT界面小程序开发

背景&#xff1a;需要在linux不同环境下可以测试我们的读卡器设备 搭建本地linux开发环境&#xff08;本来想VS里开发然后通过SSH的方式在linux下编译&#xff0c;但是工具链一直没搞起来&#xff0c;所以我是在ubuntu里安装的QT Creator工具直接开发的&#xff09;&#xff1b…

ARMday6作业

1&#xff0c;串口字符串收发现象实现图 2.串口控制灯亮灭 main.c #include "uart4.h"//封装延时函数 void delay(int ms) {int i,j;for(i0;i<ms;i){for(j0;j<2000;j){}} }int strcmp(char *a1,char *a2) {int i0;while(a1[i]a2[i]){if(a1[i]\0){break;} i;}…

C# 主窗体中显示子窗体(MDI)

1.示例代码&#xff0c;假如主窗体为MainForm,有三个子窗体分别是&#xff1a;Form1&#xff0c;Form2&#xff0c;Form3 public partial class MainForm : Form {public MainForm(){InitializeComponent();}Form1 form1 new Form1(); //子窗体1Form2 form2 new Form2(); //…

【地图】腾讯地图 - InfoWindow 自定义信息窗口内容时,内容 html 嵌套混乱问题

目录 需求描述问题问题代码页面展示 解决原因解决办法解决代码页面展示 代码汇总注 需求描述 腾讯地图上画点位&#xff0c;点击点位展示弹框信息 问题 问题代码 // 打开弹框 openInfoWindow(position, content) {this.infoWindow new TMap.InfoWindow({map: this.map,posit…