前置概念
有向无环图(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.计算每个活动(边)的ETE和LTE
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)