拓扑排序的意义是解决工程的顺序进行问题,但有时我们也需要解决工程完成需要的最短时间问题。
AOV网和AOE网:
AOE(Activity On Edge)网:顶点表示事件,弧表示活动,弧上权值表示活动所需时间
网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点
在AOE网中,只有一个顶点代表的事件发生后,从该顶点出发的各个弧所代表的活动才能开始,只有以弧头关联一个顶点的各个弧所代表的活动都已结束,该顶点所代表的事件才能发生。
一项工程可以由若干个子工程活动组成。用AOV网表示这项工程所关心的是各子工程之间的优先次序,即所得到得拓扑有序序列;而用AOE网表示这项工程所关心的是完成整个工程至少需要多少时间,哪些子工程是影响这项工程进度的关键活动,如何加快整个工程的进度等问题。
由于在AOE网中某些活动可以并行进行,所以完成工程的最短时间是从源点到汇点路径的最大长度(指路径上各活动持续时间之和最大,而不是路径上弧的数目最多)。把从源点到汇点路径长度最大的路径称作关键路径(critical path),关键路径上的活动称作关键活动。关键活动的长度是整个工程的最短工期,加快关键活动的完成是加快工程进度缩短工期地关键。
也就是说只有缩短关键路径上的关键活动时间才可以减少整个工期长度
关键路径算法:
首先定义几个参数:
事件的最早发生时间ve(vertex earliest):即顶点V的最早发生时间
事件的最晚发生时间vl(vertex latest):即顶点V的最晚发生时间
活动的最早开始时间e:即弧的最早开始时间
活动的最晚开始时间l:即弧的最晚开始时间,也就是不推迟工期的最晚开始时间
判断关键活动看是否有: e(i)=l(i)
我们假设活动a(i)是弧<j,k>上的活动,
j为弧尾顶点,k为弧头(有箭头的一边)
ve(j)代表的是弧尾j的最早发生时间,vl(k)代表的是弧头k的最迟发生时间
dut(<j,k>)代表该活动要持续的时间,既是弧的权值
1.从源点开始每个活动所需要最长的时间就是事件最早开始时间ve:
ve(j)=Max{ve{i}+dut(<i,j>)};
如v1,只有v0指向它,那么最早开始时间就是3;相对于v3,v0->v2->v3=4+8=12,v0->v1->v3=3+5=8,两者比较,前者大,两者取大的,故12为最早开始时间,依次类推。
2.从汇点开始逆向拓扑排序,事件最晚开始时间vl:
vl(i)=Min{vl(j)-dut(<i,j>)};
从右边倒推,可以求的最迟开始时间,如v9为27,以v8为例,v8->v9 倒推 27-3=24 所以v8最迟开始时间为24;
v4为例,v4->v7->v8->v9 倒推27-3-5-4=15,v4->v6->v9 倒推 27-2-9=16,两者取小的,所以v4的最迟开始时间为15。
我们现在要求的就是每弧所对应的e(i)和l(i),求这两个变量的公式是:
e(i)=ve(j)
l(i)=vl(k)-dut(<j,k>)
知道了一切所需的条件之后我们可以开始着手于算法的实现了:
建立一个邻接表。
求事件最早发生时间ve就是从头至尾找拓扑序列的过程,因此在求关键路径之前需要先调用一次拓扑序列算法来计算ve和拓扑序列列表,为此先声明几个全局变量:
int *ve,*vl; //事件最早发生时间和最晚发生时间数组
int *stack2; //用于存储拓扑序列的栈
int top2; //用于stack2的栈
stack2用来存储拓扑序列,在关键路径时用到
以下是改进后的拓扑排序:
Status TopologicalSort(GraphAdjList G)
{
EdgeNode *e;
int i,k,gettop;
int top=0; //栈顶下标,空栈为0
int count=0; //统计输出的顶点数
int *stack; //该类型的栈
stack=(int *)malloc(G->numVertexes*sizeof(int)); //该条代码要注意
for(i=0;i<G->numVertexes;i++)
if(G->adjList[i].in==0) //入度为0则入栈
stack[++top]=i;
top2=0;
etv=(int *)malloc(G->numVertexes*sizeof(int));
for(i=0;i<G->numVertexes;i++)
ve[i]=0; //初始化
stack2=(int *)malloc(G->numVertexes*sizeof(int)); //初始化
while(top!=0)
{
gettop=stack[top--];
count++;
stack2[++top2]=gettop; //将弹出的顶点序号压入拓扑序列的栈
for(e=G->adjList[gettop].firstedge;e;e=e->next)
{//对此顶点的弧表进行遍历
k=e->adjvex;
--G->adjList[k].in; //将与该顶点有连线的其他顶点入度减去1
if(!G->adjList[k].in) //其他顶点入度为0时也入栈
stack[++top]=k;
/*重要代码*/
if((ve[gettop]+e->weight)>ve[k]) //求各顶点事件的最早发生时间!!
etv[k]=etv[gettop]+e->weight;
}
}
if(count<G->numVertexes)
return ERROR;
else
return OK;
}
需要注意的是:
全局变量ve,vl的int *类型,以及int *stack2的类型
第8 14 17行代码申请动态存储空间堆栈的含义
19-36行的循环中,重要if语句
关键路径算法代码:
void CriticalPath(GraphAdjList G)
{
EdgeNode *e;
int i,gettop,k,j;
int ear,last; //活动最早,最晚开始时间
TopologicalSort(G);
l=(int *)malloc(G->numVertexes*sizeof(int));
for(i=0;i<G->numVertexes;i++)
vl[i]=ve[G->numVertexes]; //初始化事件最晚时间
while(top2!=0)
{
gettop=stack2[top2--];
for(e=G->adjList[gettop].firstedge;e;e=e->next)
{//求各顶点事件的最晚发生时间vl
k=e->adjvex;
if(vl[k]-e->weight<vl[gettop])
vl[gettop]=vl[k]-e->weight;
}
}
for(j=0;j<G->numVertexes;j++) //对每个顶点循环
{
for(e=G->adjList[j].firstedge;e;e=e->next) //对每个弧表循环
{
k=e->adjvex;
ear=ve[j];
last=vl[k]-e->weight;
if(ear==last)
printf("<V%d,V%d> length:%d",G->adjList[j].data,G->adjList[k].data,e->weight);
}
}
}
第20行循环代码内是判断是否是关键路径,关键活动的语句
算法的时间复杂度:n个顶点e条边
有拓扑排序O(n+e),初始化O(n),20行也是O(n+e)
故最终是O(n+e)