7.6有向无环图应用之关键路径
关键路径
有向图在工程计划和经营管理中有着广泛的应用。通常用有向图来表示工程 计划时有两种方法:
- (1)用顶点表示活动,用有向弧表示活动间的优先关系,即上节所讨论的 AOV 网。
- (2)用顶点表示事件,用弧表示活动,弧的权值表示活动所需要的时间。
把用第二种方法构造的有向无环图叫做边表示活动的网(Activity On Edge Network),简称 AOE-网
AOE-网在工程计划和管理中很有用。在研究实际问题时,人们通常关心的是:
- 哪些活动是影响工程进度的关键活动?
- 至少需要多长时间能完成整个工程?
在 AOE 网中存在惟一的、入度为 0 的顶点,叫做源点;存在惟一的、出度为 0 的顶点,叫做汇点。从源点到汇点的最长路径的长度即为完成整个工程任务所 需的时间,该路径叫做关键路径。关键路径上的活动叫做关键活动。这些活动中 的任意一项活动未能按期完成,则整个工程的完成时间就要推迟。相反,如果能够加快关键活动的进度,则整个工程可以提前完成。
例如,在下图所示的 AOE-网中,共有 9 个事件,分别对应顶点 v0, v1, v2, …,v7, v8。其中 v0为源点,表示整个工程可以开始。事件 v4表示 a4,a5已经完成,a7,a8 可以开始。v8为汇点,表示整个工程结束。v0到v8的最长路径(关键路径)有两 条:(v0,v1,v4,v7,v8)或(v0,v1,v4,v6,v8),长度均为 18。关键活动为
(a1,a4,a7,a10)或(a1,a2,a8,a11)。 关键活动 a1计划 6 天完成,如果 a1 提前 2 天完成,则整个工程也可以提前 2 天完成。
在讨论关键路径算法之前,首先给出几个重要的定义:
- (1)事件 vi的最早发生时间 ve(i):从源点到顶点 vi的最长路径的长度,叫做 事件 vi的最早发生时间。
求 ve(i) 的值可从源点开始,按拓扑顺序向汇点递推:
ve(0)=0;
ve(i)=Max{ ve(k)+dut(<k,i>)}
<k,i>∈T,1≤i≤n-1;
其中,T 为所有以 i 为头的弧<k,i>的集合,dut(<k,i>)表示与弧<k,i>对 应的活动的持续时间。
- (2)事件 vi的最晚发生时间 vl(i):在保证汇点按其最早发生时间发生这一前提下,求事件 vi的最晚发生时间。
在求出 ve(i)的基础上, 可从汇点开始,按逆拓扑顺序向源点递推,求出 vl(i):
vl(n-1)=ve(n-1);
vl(i)=Min{vl(k)+dut(<i,k>)}
<i,k>∈S,0≤i≤n-2;
其中,S 为所有以 i 为尾的弧<i,k>的集合,dut(<i,k>)表示与弧<i,k>对应的 活动的持续时间。
- (3)活动 ai的最早开始时间 e(i):如果活动 ai 对应的弧为<j,k>,则 e(i)等于从源点到顶点 j 的最长路径的长度,即:e(i)=ve(j)
- (4)活动 ai的最晚开始时间 l(i):如果活动 ai对应的弧为<j,k>,其持续时间为 dut(<j,k>)则有:l(i)=vl(k)- dut(<j,k>) 即在保证事件vk的最晚发生时间为vl(k)的前提下,活动ai的最晚开始时间为l(i)
- (5)活动 ai的松弛时间(时间余量):ai的最晚开始时间与 ai的最早开始时间之差:l(i)- e(i)。 显然,松弛时间(时间余量)为 0 的活动为关键活动
求关键路径的基本步骤如下:
①对图中顶点进行拓扑排序,在排序过程中按拓扑序列求出每个事件的最早 发生时间 ve(i);
②按逆拓扑序列求每个事件的最晚发生时间 vl(i);
③求出每个活动 ai的最早开始时间 e(i)和最晚发生时间 l(i); ④找出 e(i)=l(i) 的活动 ai,即为关键活动。
下面首先修改上一节的拓扑排序算法,以便同时求出每个事件的最早发生时 间 ve(i):
【算法思想】
- (1) 首先求出各顶点的入度,并将入度为 0 的顶点入栈 S;
- (2) 将各顶点的最早发生时间 ve[i]初始化为 0;
- (3) 只要栈 S 不空,则重复下面处理:
①将栈顶顶点 j 出栈并压入栈 T(生成逆拓扑序列);
②将顶点 j 的每一个邻接点 k 的入度减 1,如果顶点 k 的入度变为 0,则将 顶点 k 入栈;
③根据顶点 j 的最早发生时间 ve[j]和弧<j, k>的权值,更新顶点 k 的最早发 生时间 ve[k]。
【算法描述】 修改后的拓扑排序算法
int ve[MAX_VERTEX_NUM]; /*每个顶点的最早发生时间*/
int TopoOrder(AdjList G,Stack * T) /* G 为有向网,T 为返回拓扑序列的栈,S 为存放入度为 0 的顶点的栈*/
{
int count,i,j,k;
ArcNode *p;
int indegree[MAX_VERTEX_NUM]; /*各顶点入度数组*/
Stack S;
InitStack(T);
InitStack(&S); /*初始化栈 T, S*/
FindID(G, indegree); /*求各个顶点的入度*/
for(i=0;i<G.vexnum;i++)
if(indegree[i]==0)
Push(&S,i);
count=0;
for(i=0;i<G.vexnum;i++)
ve[i]=0; /*初始化最早发生时间*/
while(!IsEmpty(&S))
{
Pop(&S,&j);
Push(T,j);
count++;
p=G.vertex[j].firstarc;
while(p!=NULL)
{
k=p->adjvex;
if(--indegree[k]==0)
Push(&S,k); /*若顶点的入度减为 0,则入栈*/
if(ve[j]+p->Info.weight>ve[k])
ve[k]=ve[j]+p->Info.weight;
p=p->nextarc;
} /*while*/
} /*while*/
if (count<G.vexnum)
return(Error);
else
return(Ok);
}
有了每个事件的最早发生时间,就可以求出每个事件的最迟发生时间,进一 步可求出每个活动的最早开始时间和最晚开始时间,最后就可以求出关键路径了。
求关键路径的算法实现如下:
【算法思想】
- (1) 首先调用修改后的拓扑排序算法,求出每个事件的最早发生时间和逆拓扑 序列栈 T;
- (2) 将各顶点的最晚发生时间 vl[i]初始化为汇点的最早发生时间;
- (3) 只要栈 T 不空,则重复下面处理: ①将栈顶顶点 j 出栈; ②对于顶点 j 的每一个邻接点 k,根据顶点 k 的最晚发生时间 vl[k]和弧<j, k> 的权值,更新顶点 j 的最晚发生时间 vl[j]。
- (4) 扫描每一条弧,计算其最早发生时间 ei 和最晚发生时间 li,如果 ei 等于 li 则输出该边。
【算法描述】 关键路径算法
int CriticalPath(AdjList G)
{
ArcNode *p;
int i,j,k,dut,ei,li;
char tag; int vl[MAX_VERTEX_NUM]; /*每个顶点的最迟发生时间*/
Stack T;
if(!TopoOrder(G, &T))
return(Error);
for(i=0; i<G.vexnum; i++)
vl[i]=ve[G.vexnum-1]; /* 将各顶点事件的最迟发生时间初始化为 汇点的最早发生时间 */
while(!IsEmpty(&T)) /*按逆拓扑顺序求各顶点的 vl 值*/
{
Pop(&T,&j);
p=G.vertex[j].firstarc;
while(p!=NULL)
{
k=p->adjvex;
dut=p->weight;
if(vl[k]-dut<vl[j])
vl[j]= vl[k]-dut;
p=p->nextarc;
} /* while */
} /* while*/
for(j=0;j<G.vexnum;j++) /*求 ei,li 和关键活动*/
{
p=G.vertex[j].firstarc;
while(p!=NULL)
{
k=p->Adjvex;
dut=p->Info.weight;
ei=ve[j];
li=vl[k]-dut;
tag = (ei==li) ? '*' : ' ' ; /*标记并输出关键活动*/
printf("%c,%c,%d,%d,%d,%c\n",G.vertex[j].data,G.vertex[k].data,dut,ei,li,ta g);
p=p->nextarc;
} /*while*/
} /* for */
return(Ok);
} /*CriticalPath*/
算法的时间复杂度为 O(n+e)。用该算法求上图中 AOE-网的关键路径,结果 如下所示。
例如,对下图所示的 AOE 网计算关键路径过程如下: