圖的拓補排序與關鍵路徑

概述

       前篇博文講了有環圖的兩種應用:最小生成樹(給出一張地圖,規劃去所有地點的最小代價和以及路線)以及最短路徑(從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);
		}
	}
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章