拓扑排序及关键路径(数据结构)

前置概念

有向无环图(Directed Acyclic Graph,简称 DAG):没有回路的有向图,常用于工程/项目/流程管理

**AOV网:**用于表示项目/工程的DAG图,在该图中顶点表示活动,有向边表示活动之间的先后次序/依赖关系

对DAG图,当我们遇到需要依赖排序(先完成前置任务,再执行后续任务)的情况时,常使用拓扑排序。(PS:DAG图至少存在一个拓扑序列)


拓扑序列及拓扑排序

**拓扑序列:**设G=(V,E)是一个具有n个顶点的有向图,V中的顶点V1、V2、V3...Vn,满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必在Vj之前。称这样的顶点序列为拓扑序列
**拓扑排序:**在DAG图中构造一个拓补序列的过程
拓扑排序的方法:

1.从DAG图中找到一个入度为0 的顶点

2.将该顶点加入拓补序列,然后从图中删除该顶点及其出边

3.重复操作1和操作2,直到图中再无顶点为止

PS:若最终图中还剩余顶点,则该图不是DAG图

拓扑排序代码实现(C++)

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
using namespace std;

//边表节点结构
struct ENode{
	int data_i;//邻接点的下标
	ENode* next; 
}; 
//顶点表
struct Vertex{
	char data;
	ENode* first;
};

int n,m;//顶点数 边数
vector<Vertex>Graph;//存图
vector<int>ind;//入度数组
vector<int>topo;//存拓扑序列 

//拓扑排序 
bool Toposort(){
	stack<int> s;
	
	//遍历图中所有的点,将所有入度为0的顶点下标入栈
	for(int i=0;i<n;i++){
		if(ind[i]==0){
			s.push(i);
		}
	}
	
	//循环,将入度为0的顶点出栈,输出该顶点/将该顶点加入到拓扑序列中,该顶点的邻接点入度-1,若有邻接点的入度为0,则将该邻接点入栈 
	while(!s.empty()){
		int i=s.top();//获取顶点下标 
		s.pop();//栈顶元素出栈
		topo.push_back(i);//将该顶点加入到拓扑序列中 
		
		//遍历该顶点的邻接边表
		ENode* p = Graph[i].first;
		while(p!=nullptr){
			int a=p->data_i;
			ind[a]--;//邻接点的入度-1
			
			//该邻接点入度为0,将其入栈 
			if(ind[a]==0){
				s.push(a);
			}
			
			p=p->next;
		} 
	} 
	
	//该图为DAG图 
	if(topo.size()==n){
		return true;
	}
	//该图不是DAG图 
	else{
		return false;
	}
}

//查找顶点x在图中的下标 
int Find(char x){
	for(int i=0;i<n;i++){
		if(Graph[i].data==x){
			return i;
		}
	}
}

int main(){
	cin>>n>>m;
	
	Graph.resize(n+1);//更新Graph容量
	ind.resize(n+1);//更新入度数组容量 
	
	//更新顶点表
	for(int i=0;i<n;i++){
		cin>>Graph[i].data;
		Graph[i].first=nullptr;
	}
	
	//更新邻接边表
	for(int i=1;i<=m;i++){
		//存边 
		char x,y;
		cin>>x>>y;
		
		//查找顶点x和顶点y在Graph中的下标 
		int x_i,y_i;
		x_i=Find(x);
		y_i=Find(y);
		
		ind[y_i]++;//顶点y的入度+1
		
		//更新顶点x的邻接边表 
		ENode* node = new ENode();
		
		node->data_i= y_i;
		//头插 
		node->next = Graph[x_i].first;
		Graph[x_i].first = node;
	} 
	
	//进行拓扑排序 
	bool flag = Toposort();
	
	if(flag){
		cout<<"该图的拓扑排序结果如下:"<<endl;
		for(int i=0;i<topo.size();i++){
			int t_i = topo[i];
			cout<<Graph[t_i].data<<" ";
		}
		cout<<endl;
	}
	else{
		cout<<"该图不是有向无环图,没有拓扑序列!"<<endl;
	}
	
	return 0;
}
/*
6 8
A B C D E F
A B
A C
A D
C B
C E
F D
F E
D E
该图的拓扑排序结果如下:
F A C B D E


6 8
A B C D E F
A B
A C
A D
C B
C E
F D
E F
D E
该图不是有向无环图,没有拓扑序列!
*/

拓扑排序函数的时间复杂度:O(n + m)(邻接表存图)或O(n^2)(邻接矩阵存图)


关键路径

关键路径: DAG图中从起点到终点的最长路径, 顶点表示事件,边表示活动,该路径的总权值 = 工程的最短完成时间。其中起点是唯一入度为0的顶点,终点是唯一出度为0的顶点。
名词解释:
ETV:
Earliest Time of Vertex,事件的
最早
发生时间
LTV: Latest Time of Vertex,事件的最晚发生时间
ETE:Earliest Time of Edge,活动最早可以开始的时间
LTE:Latest Time of Edge,活动最晚必须开始的时间
求关键路径步骤:

1.拓扑排序并计算每个事件(顶点)最早发生时间ETV

2.逆拓扑排序并计算每个事件(顶点)最晚发生时间LTV

3.计算每个活动(边)的ETELTE

4.确定关键活动与关键路径** **

关键路径代码实现(C++)

cpp 复制代码
//关键路径代码实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
using namespace std;

//边表节点结构
struct ENode{
	int adj;//邻接点的下标
	int w;//边权
	ENode* next;//链式邻接表指针域 
}; 

//顶点表
struct Vertex{
	char data;//顶点数据 
	ENode* first;//指向该顶点的第一条出边(邻接边链表的头指针) 
};

int n,m;//顶点数 边数
vector<Vertex> Graph;//存图
vector<int> ind;//入度数组
vector<int> topo;//存拓扑序列
vector<int> etv;//点的最早发生时间(Earliest Time of Vertex)
vector<int> ltv;//点的最晚发生时间(Latest Time of Vertex)

//拓扑排序
void TopoSort(){
	stack<int> s;//定义一个栈来存储当前入度为0的顶点 
	
	//遍历图中所有的顶点,将所有入度为0的顶点下标入栈
	for(int i=1;i<=n;i++){
		if(ind[i]==0){
			s.push(i);
		}
	}
	
	//循环,将入度为0的顶点出栈,输出该顶点/将该顶点加入到拓扑序列中,该顶点的邻接点入度-1,若有邻接点的入度为0,则将该邻接点入栈
	while(!s.empty()){
		int i=s.top();//获取顶点下标
		s.pop() ;//栈顶元素出栈
		topo.push_back(i);//将该顶点加入到拓扑序列中
		
		//遍历该顶点的邻接边表(即顶点i的所有出边) 
		ENode* p=Graph[i].first;
		while(p != nullptr){
			int a = p->adj;
			ind[a]--;//邻接点的入度-1
			
			//若此时该邻接点的入度为0,将其入栈 
			if(ind[a] == 0){
				s.push(a);
			} 
			
			//更新邻接点a的最早发生时间 
			//事件a的最早发生时间 = max(事件i的最早发生时间 + 活动i->a所需时间) 
			etv[a] = max(etv[a], etv[i] + p->w);
 			
 			p=p->next;//遍历下一条出边 
		} 	
	}	
}

//查找顶点x在图中的下标
int Find(char x){
	for(int i=1;i<=n;i++){
		if(Graph[i].data == x){
			return i;//找到了,返回该顶点在Graph数组中的下标 
		}
	}
	
	return -1;//图中没找到该顶点 
} 

//关键路径 
void CriticalPath(){
	//终点(拓扑序列的最后一个顶点) 
	int ed = topo[topo.size() - 1];
	
	//先初始化ltv数组,将每个顶点的最晚发生时间都初始化为etv[ed],即终点的最早发生时间 
	//因为整个工程不能晚于终点的最早完成时间 
	for(int i=1; i<=n; i++){
		ltv[i] = etv[ed];
	} 
	
	ENode* p = nullptr;
	int i,j;
	
	//倒着遍历topo数组,更新ltv[i]
	//从倒数第二个顶点开始 
	for(int q = topo.size() - 2; q >= 0;q--){
		i = topo[q];//当前处理的顶点 
		p = Graph[i].first;//获取顶点i的第一条出边 
		while(p != nullptr){
			j = p->adj;//j是i的邻接点 
			
			//更新顶点i的最晚发生时间
			//ltv[i] = min(ltv[i], ltv[j] - 活动 i->j 所需时间)
			//也就是说i的最晚完成时间不能影响j的最晚开始时间 
			ltv[i] = min(ltv[i],ltv[j] - p->w);
			p = p->next;
		}
	}
	
	//遍历所有的边,找关键活动
	//关键活动:最早开始时间 == 最晚开始时间(没有机动时间) 
	int ete,lte;//活动的最早开始时间   活动的最晚开始时间 
	
	//遍历每个顶点作为活动的起点 
	for(int i=1;i<=n;i++){
		//遍历顶点i的所有出边,即以i为起点的所有活动
		for(p = Graph[i].first; p != nullptr; p=p->next){
			j = p->adj;//j是活动的终点 
			
			//活动i->j的最早开始时间 = 事件i的最早发生时间
			ete = etv[i];
			
			//活动i->j的最晚开始时间 = 事件j的最晚发生时间 - 活动所需时间(边权) 
			lte = ltv[j] - p->w;
			
			//如果最早开始时间等于最晚开始时间,则该活动是关键活动 
			if(ete == lte){
				cout << "关键活动: " << Graph[i].data << " -> " << Graph[j].data << endl;
			}
		} 
	} 
}

int main(){
	cin>>n>>m;
	
	Graph.resize(n+1);//更新Graph容量
	ind.resize(n+1,0);//更新入度数组容量,初始化为0
	etv.resize(n+1,0);//初始化最早发生时间为0
	ltv.resize(n+1,0);//初始化最晚发生时间为0
	
	//更新顶点表
	for(int i=1;i<=n;i++){
		cin>>Graph[i].data;
		Graph[i].first=nullptr;
	} 
	
	//更新邻接边表
	for(int i=1;i<=m;i++){
		char x,y;
		int w;
		cin>>x>>y>>w;
		
		//查找顶点x和顶点y在Graph中的下标
		int x_i,y_i;
		x_i=Find(x);
		y_i=Find(y);
		
		ind[y_i]++;//顶点的入度+1
		
		//更新顶点x的邻接边表
		ENode* node = new ENode();
		
		node->adj=y_i;
		node->w=w;
		
		//头插法向表中插入节点 
		node->next=Graph[x_i].first;
		Graph[x_i].first=node; 
	}
	cout<<"输出结果如下:"<<endl;
	
	//进行拓扑排序
	TopoSort();
	
	//求关键路径
	CriticalPath();
	 
	
	return 0;
}
/*
9 11
A B C D E F G H Y
A B 6
A C 4
A D 5
B E 1
C E 1
D F 2
E G 9
E H 7
F H 4
G Y 2
H Y 4
输出结果如下:
关键活动: A -> B
关键活动: B -> E
关键活动: E -> H
关键活动: E -> G
关键活动: G -> Y
关键活动: H -> Y
*/

关键路径的时间复杂度为O(n + m) ,拓扑排序及逆拓扑排序时间复杂度为O(n + m) ,计算 ETE / LTE 并判断关键活动遍历了每条边,所以时间复杂度为O(m) ,综合起来得到关键路径的时间复杂度为O(n + m)

相关推荐
qwehjk20082 小时前
实时语音处理库
开发语言·c++·算法
2301_804215412 小时前
自定义异常类设计
开发语言·c++·算法
c++逐梦人2 小时前
C++11 ——— 右值引用和移动语义
c++·右值
暮冬-  Gentle°2 小时前
C++代码依赖分析
开发语言·c++·算法
糯诺诺米团2 小时前
C++多线程打包成so给JAVA后端(Ubuntu)<3>
java·开发语言·c++
2301_763891952 小时前
泛型编程与STL设计思想
开发语言·c++·算法
j_xxx404_2 小时前
蓝桥杯基础--进制转换
开发语言·数据结构·c++·算法·职场和发展·蓝桥杯
雪域迷影2 小时前
OpenHarmony 电源管理模块状态转换分析
c++·openharmony·电源管理部件
Yu_Lijing2 小时前
基于C++的《Head First设计模式》笔记——解释器模式
c++·设计模式·解释器模式