图的拓补排序与关键路径

概述

       前篇博文讲了有环图的两种应用:最小生成树(给出一张地图,规划去所有地点的最小代价和以及路线)以及最短路径(从A点坐地铁到B点应该如何换乘代价最小),现在来谈谈无环图的应用。无环,即是图中没有回路的意思。一个无环的有向图称为无环图(Directed Acyclic Graph),简称DAG图。

拓补排序

       所有的工程或者某种流程都可以分为若干个小的工程或者阶段,称这些小的工程或阶段为“活动”。

       这些子程序之间存在一定的约束,其中某种子工程的开始必须在另一些子工程完成之后。因此DAG图表示一个工程,其中有向边表示约束关系。这种有向图必须是无环的。如果出现了环(有向环),那么向前递推,环路上的任一子工程开始的先决条件必然是自己,显然矛盾的。如果设计出这样的工程图,工程无法进行。

       拓扑排序就是测试一个工程能否顺利进行。

       若以图中的顶点来表示活动,有向边表示活动之间的优先关系,则这样的有向图称为 AOV(Activity On Vertex network) 网。

       在 AOV 网中,若从顶点 v i 到顶点 v j 之间存在一条有向路径,称顶点 v i 是顶点 v j 的前驱,或者称顶点 v j 是顶点 v i 的后继。若是图中的弧,则称顶点 v i 是顶点 v j 的直接前驱,顶点 v j 是顶点 v i 的直接后继。

       对 AOV 网进行拓扑排序的方法和步骤如下:

      1. 从 AOV 网中选择一个没有前趋的顶点(该顶点的入度为 0 )并且输出它;
      2. 从网中删去该顶点,并且删去从该顶点发出的全部有向边;
      3. 重复上述两步,直到剩余网中不再存在没有前趋的顶点为止。

      操作的结果有两种:
     一种是网中全部顶点都被输出,这说明网中不存在有向回路,拓扑排序成功;
     另一种是网中顶点未被全部输出,剩余的顶点均有前趋顶点,这说明网中存在有向回路,不存在拓扑有序序列。

     下面给出实例,加深对上述算法的理解:

     这样得到一个拓朴序列 v 1 , v 6 , v 4 , v 3 , v 2 , v 5 。 很显然这不是唯一的拓补排序方案,不过我们只要知道了拓补排序成功就证明了工程能够顺利进行。

    考虑到算法过程中始终要扫描入度为0的顶点,我们在原来的顶点表结构中增加in作为入度域。同时为了避免在每一步选入度为0的顶点时重复扫描表头数组,利用表头数组中入度为0的顶点域作为链栈域,存放下一个入度为零的顶点序号。

    拓朴排序算法梗概如下:
    扫描顶点表,将入度为零的顶点入栈;
    While ( 栈非空 )
    {

      将栈顶点 v j 弹出并输出之;
      在邻接链表中查 v j 的直接后继 v k ,把 v k 的入度减 1 ,若 v k 的入度为零则进栈;
    }

    将本算法应用于上图得到如下的详细过程分析:

    扫描顶点表,将度为0的v1,v6压入栈中,将v1输出,对v2,v3,v4的入度减一,发现v3入度为0,v3进栈----此时栈内v6,v3;

    将栈顶元素v6输出,对v4,v5入度减一,发现v4入度为0,v4进栈---此时栈内v3,v4;

    将栈顶元素v3输出,对v2,v5入度减一,发现v2入度为0,v2进栈---此时栈内v4,v2;

    将栈顶元素v4输出,对v5入度减一,发现v5入度为0,v5进栈---此时栈内v2,v5;

    栈顶元素v2、v5依序输出;最终栈为空跳出循环;

    输出结果:v1,v6,v3,v4,v2,v5,拓补排序成功。

Status TopologicalSort(GraphAdjList GL)
{
	EdgeNode *e;
	int i,k,gettop;
	int top=0;//用于栈底指针下标
	int count=0;//用于统计输出顶点个数
	int *stack;
	stack = (int*)malloc(GL->numVertexes * sizeof(int));
	for(int i=0 ; i<GL->numVertexes ; i++)
		if(GL->adjList[i].in == 0)
			stack[++top] = i;//将入度为0的顶点入栈
	while(top != 0)//栈不空
	{
		gettop = stack[top--];//出栈
		printf("%d -> " , GL->adjList[gettop].data);//输出
		count++;
		for(e=GL->adjList[gettop].firstedge ; e ; e=e->next){
			//对此顶点边表遍历
			k = e->adjvex;
			if((--GL->adjList[k].in) == 0)//将k号顶点邻接点的入度减一
				stack[++top] = k;//若为0则入栈
		}
	}
	if(count < GL->numVertexes)
		return ERROR;
	return OK;
}

     分析整个算法,对于一个具有n个顶点e条弧的AOV网而言,扫描顶点表入栈花去O(n),内循环复杂度O(e),因此算法复杂度O(n+e)。


关键路径


      若在带权的有向图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续时间),则此带权的有向图称为边表示活动的网 (Activity on Edge Network) ,简称 AOE 网。

     表示实际工程计划的 AOE 网应该是无环的,并且存在唯一的入度过为 0 的开始顶点和唯一的出度为 0 的完成顶点。

     拓补排序与关键路径:拓补排序算法主要解决一个工程是否能顺利进行的问题,关键路径算法解决工程完成需要的最短时间问题(找到最关键的流程);

     AOV网与AOE网:AOV网顶点是表示活动的网,只描述活动中的制约关系;AOE网是用边表示活动的网,边上的权值表示活动的持续时间。AOE网需要建立在活动之间的制约关系没有矛盾的基础上,再来分析完成整个工程至少需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动等问题。

    关键路径:将各个活动持续时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动称为关键活动。并不是加快任何一个关键活动都可以缩短整个工程完成的时间,只有加快那些包括在所有的关键路径上的关键活动才能达到这个目的。只有在不改变 AOE 网的关键路径的前提下,加快包含在关键路径上的关键活动才可以缩短整个工程的完成时间。

    假设开始点是v1,从v1到vi的最长路径叫做时间vi的最早发生时间。这个时间决定了所有以vi为尾的弧所表示的活动的最早开始时间。我们用e(i)表示活动ai的最早开始时间。

   还可以定义一个活动开始的最迟时间l(i),这是在不推迟整个工程完成的前提下,活动ai最迟必须开始进行的时间。两者之差l(i)-e(i)意味着完成活动ai的时间余量。当这个时间余量等于0的时候,也即是l(i)=e(i)的活动,我们称其为关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程的进度。


   由上面的分析可知,辨别关键活动就是要找e(i)=l(i)的活动。为了求得e(i)和l(i),首先应求得事件的最早发生时间ve(j)和最迟发生时间vl(j)。
   求解ve(j)和vl(j)需分两个步进行:
(1) 从ve(0)=0头部开始向后推进求得ve(j)
Ve(j) = Max{ve(i) + dut(<i,j>) } ; <i,j>属于T,j=1,2...,n-1
其中T是所有以第j个顶点为头的弧的集合。

(2) 从vl(n-1) = ve(n-1)底部起向前推进求得vl(j)
vl(i) = Min{vl(j) - dut(<i,j>} ; <i,j>属于S,i=n-2,...,0
其中,S是所有以第i个顶点为尾的弧的集合。
   这两个递推公式的计算必须分别在拓扑有序和逆拓扑有序的前提先进行。也就是说,ve(j-1)必须在vj的所有前驱的最早发生时间求得之后才能确定,而vl(j-1)必须在Vj的所有后继的最迟发生时间求得之后才能确定。因此可以在拓扑排序的基础上计算ve(j-1)和vl(j-1)。

int *etv,*ltv;//事件最早发生时间和最迟发生时间
int *stack2;  //存储拓补序列的栈
int top2;     //用于stack2的指针
Status TopologicalSort(GraphAdjList GL)
{
	EdgeNode *e;
	int i,k,gettop;
	int top=0;//用于栈底指针下标
	int count=0;//用于统计输出顶点个数
	int *stack;
	stack = (int*)malloc(GL->numVertexes * sizeof(int));
	for(int i=0 ; i<GL->numVertexes ; i++)
		if(GL->adjList[i].in == 0)
			stack[++top] = i;//将入度为0的顶点入栈
	//---相对于拓补排序加入的部分1
	top2 = 0;
	etv = (int*)malloc(GL->numVertexes * sizeof(int));
	for(int i=0 ; i<GL->numVertexes ; i++)
		etv[i] = 0;
	stack2 = (int *)malloc(GL->numVertexes * sizeof(int));
	//--------------------------
	while(top != 0)//栈不空
	{
		gettop = stack[top--];//出栈
		count++;
		stack2[++top2] = gettop;//将弹出的顶点序号压入拓补序列的栈
		for(e=GL->adjList[gettop].firstedge ; e ; e=e->next){
			//对此顶点边表遍历
			k = e->adjvex;
			if((--GL->adjList[k].in) == 0)//将k号顶点邻接点的入度减一
				stack[++top] = k;//若为0则入栈
			//---相对于拓补排序加入的部分2
			if(etv[gettop]+e->weight > etv[k])//求各顶点事件最早发生时间
				etv[k] = etv[gettop]+e->weight;
			//--------------------------
		}
	}
	if(count < GL->numVertexes)
		return ERROR;
	return OK;
}
//求关键路径
void CriticalPath(GraphAdjList GL)
{
	EdgeNode *e;
	int i,k,gettop;
	int ete,lte;
	TopologicalSort(GL);//计算etv和拓补序列stack2的值
	ltv = (int *)malloc(GL->numVertexes * sizeof(int));
	for(i=0 ; i<GL->numVertexs ; i++)
		ltv[i] = etv[GL->numVertexs-1];//初始化ltv
	while(top2 != 0){
		gettop = stack2[top2--];//将拓补序列出栈,逆序得到顶点号
		for(e=GL->adjList[gettop].firstedge ; e ;e=e->next){
			//求各顶点最迟发生时间的ltv值
			k = e->adjvex;
			if(ltv[k] - e->weight < ltv[gettop])
				ltv[gettop] = ltv[k]-e->weight;
		}
	}
	//---求ete、lte和关键活动
	for(int j=0 ; j<GL->numVertexes ; j++){//遍历每个顶点
		for(e=GL->adjList[gettop].firstedge ; e ;e=e->next){//对每个顶点遍历所有有连接的顶点
			k = e->adjvex;
			ete = etv[j];//活动最早发生时间
			lte = ltv[k]-e->weight;//活动最迟发生时间
			if(ete == lte)//活动发生在关键路径上
				printf("<%d,%d> length: %d",GL->adjList[j].data,GL->adjList[k].data,e->weight);
		}
	}
}



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值