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。关键活动为

spark有向无环图生成 有向无环图关键路径_数据结构


(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-网的关键路径,结果 如下所示。

spark有向无环图生成 有向无环图关键路径_数据结构_02


例如,对下图所示的 AOE 网计算关键路径过程如下:

spark有向无环图生成 有向无环图关键路径_Stack_03

spark有向无环图生成 有向无环图关键路径_v8_04

spark有向无环图生成 有向无环图关键路径_数据结构_05