数据结构-----图的拓扑排序和关键路径算法

时间:2022-11-25 21:18:51

部分图片取自:http://www.cnblogs.com/navorse/articles/1893863.html


在介绍拓扑排序和关键路径之前,先引入AOE网络的概念:

数据结构-----图的拓扑排序和关键路径算法

该图为一个AOE网,顶点表示事件,如v1,v2,v3...。弧表示活动,如a1,a2,a3...,而每一条边表示的值为完成该活动所需的时间。

比如上图中,完成活动a1需要6个单位的时间,完成活动a3需要5个单位的时间。

AOE网络是一个加权有向图,即每一条边都是带方向且带有权值的。对于一个有向边,箭头指向的点为终点,另一个点则是起点。

顶点的入度是指所有以该顶点为终点的有向边的个数。如上图中以顶点v5为终点的有向边为a4和a5,所以顶点v5的入度为2。

顶点的出度是指所有以该顶点为起点的有向边的个数。如上图中以顶点v1为起点的有向边为a1,a2和a3,所以顶点v1的出度为3。


我们把入度为0的顶点叫做源点,出度为0的顶点叫做汇点。上图中v1是源点,v9是汇点。

可以理解为从源点出发,虽然中间会有很多不同的分岔口,但最终都会到达汇点。一个AOE网络至少包含一个源点和一个汇点。


概念说完了,接下来讨论一下AOE网络的性质:

对于AOE网络中的某一个顶点(事件)来说,只有当所有以该顶点为终点的有向边(活动)都完成后,该顶点代表的事件才能够发生。

同时只有某一个顶点(事件)发生后,以该顶点为起点的有向边(活动)才能够开始。

对于图中的顶点(事件)v5来说,只有边(活动)a4和a5完成后,事件v5才可以发生,边(活动)a7和a8才能开始。

一个AOE网络很形象的描述了在一个大的工程中的每一个子工程的前驱工程是什么,以及其后续工程是什么。同时根据每一个活动(边)的权值,我们可以知道某一事件发生的时间是什么。


接下来说一下拓扑排序,实现拓扑排序的步骤如下:

步骤1:找到AOE网络中入度为0的顶点,输出它。如果找不到,停止排序。

步骤2:删除所有与该点关联的边,重复步骤1。

步骤3:如果输出顶点个数小于总顶点个数,则证明图中存在环,没有关键路径

数据结构-----图的拓扑排序和关键路径算法

拓扑排序的结果叫做拓扑序列,一个AOE网络的拓扑序列不止一种


接下来讨论关键路径,先明确几个名词。

关键路径:从源点到汇点的路径长度最大的路径叫关键路径。

事件的最早发生时间ve:因为对于某一个事件(顶点)来说,其入度可能大于1,所以从源点到达该顶点的路就不止一条。对于其中一条路来说,经过的所有边的权值的和就是对这条路来说,到达该顶点需要的时间。把到达该顶点所有路的时间进行比较,取最大值,就是该事件的最早发生时间。

事件的最迟发生时间vl:值在不耽误整个工程完成时间的前提下,某一事件最晚的发生时间。这个通常比较难理解,举个例子说明一下。

比如说有三个事件a,b,c。a和b是c的前驱事件,完成活动ac需要的时间是2小时,完成活动bc需要的时间是3小时。假设现在时间是下午1点,此时事件a和事件b都已经发生,根据前面的描述可知,活动ac和bc都可以开始了。但是因为bc需要完成的时间长,所以事件c发生的时间是下午4点。那么因为ac只需两个小时就可以完成,那么事件a发生的时间可以是下午2点,再加上ac完成时间2个小时也是下午4点,并没有耽误到事件c的发生,这就是所谓的最晚发生时间。所以事件a的最早发生时间是下午1点,最晚发生时间是下午2两点。

活动开始的最早时间e:因为事件发生的同时与该事件关联的活动也就可以开始。所以一个活动(边)的最早开始时间和其起点所代表的事件的最早开始时间相同。

活动开始的最晚时间l:事件的最晚发生时间是将事件推迟,活动的最晚开始时间是将活动推迟,二者类似。

关键活动:e和l相等的活动称为关键活动。

所以求关键路径的问题就是求所有关键活动的问题。

示例如下:

数据结构-----图的拓扑排序和关键路径算法

数据结构-----图的拓扑排序和关键路径算法

接下来就是拓扑排序和关键路径的实现了。

首先需要考虑的是AOE网络的存储,需要一个图的类,可以用邻接表的方式实现。

它存在一个公有函数eraseEdge(int v1, int v2),可以删除边<v1,v2>。

一个公有函数inDegree(int v),返回顶点v的入度。

一个公有函数getFirstNode(int v),返回与顶点v关联的边的邻接表的表头指针。

其次需要考虑用什么存储拓扑序列,考虑到在求拓扑序列时汇点是最后一个被存储,而最晚发生时间是从后向前求得,汇点应该首先被弹出,所以选用栈来存储。

同时保存入度为0的顶点时也可以用栈来存储,不同的存储方式得出的序列不同(拓扑序列不止一个)。

最后需要两个数组,一个存储事件的最早发生时间,一个存储事件的最晚发生时间。

#define MAX_VEX 10
linearStack<int> stack2;

bool TopologicalSort(linkedWDigigraph theGraph, int *pEtv)
{
linearStack<int> stack1;

for(int i = 0; i<=MAX_VEX; ++i) //初始化最早发生时间
pEtv[i] = 0;

for(int i = 1; i<=MAX_VEX; ++i)
{
if(theGraph.inDegree(i) == 0)//首先寻找入度为0的顶点
stack1.push(i);
}

int v1;
int nCnt(0);
chainNode<int>* pNode;
while(!(stack1.empty()))
{
v1 = stack1.top();
stack1.pop();
stack2.push(v1);//保存拓扑序列

if(++nCnt == MAX_VEX)//判断是否有环
break;
pNode = theGraph.getFirstNode(v1);
int v2, weight;
while(pNode != NULL)
{
v2 = pNode->element;
weight = pNode->weight;
pNode = pNode->next;
theGraph.eraseEdge(v1, v2);
if(theGraph.inDegree(v2) == 0)//删除和顶点v1关联的边,再次寻找入度为0的顶点
stack1.push(v1);

if(pEtv[v2] < weight + pEtv[v1])//更新最大值
pEtv[v2] = weight + pEtv[v1];
}
}
return nCnt == MAX_VEX;//判断是否有环
}

stack2存储着拓扑序列,供关键路径函数使用:


void CriticalPath(linkedWDigraph& theGraph, int *pEtv, int *pLtv)
{
for(int i = 1; i<=MAX_VEX; ++i)
pLtv[i] = pEtv[MAX_VEX];//初始化最晚发生时间

int v1;
chainNode<int>* pNode = NULL;
while(!(stack2.empty()))
{
v1 = stack2.top();
stack2.pop();
pNode = theGraph.getFirstNode(v1);
int v2;
while(pNode != NULL)
{
v2 = pNode->element;
if(lEtv[v1] > lEtv[v2] - pNode->weight)//更新最晚发生时间
lEtv[v1] = lEv[v2] - pNode->weight;
pNode = pNode->next;
}
}
int ete(0);
int lte(0);
for(int i= 1; <= MAX_VEX; ++i)
{
pNode = theGraph.getFirstNode(i);
while(pNode != NULL)
{
ete = pEtv[i];
lte = pLtv[pNode->element] - pNode->weight;
if(ete == lte)//判断最早发生时间和最晚发生时间是否相等
{
cout << "<v" << i << ",v" << pNode->element << "> :" << pNode->weight << endl;
}
pNode = pNode->next;
}
}
}