數據結構(20)--DAG應用之AOE網的關鍵路徑

參考書籍:數據結構(C語言版)嚴蔚敏吳偉民編著清華大學出版社

本文中的代碼可從這裏下載:https://github.com/qingyujean/data-structure

1.關鍵路徑

對整個工程和系統,人們關心的是兩個方面的問題:
    1)工程能否順利進行
       對AOV網進行拓撲排序
    2)估算整個工程完成所必須的最短時間
       對AOE網求關鍵路徑

    AOE-網(Activity On Edge Network):即邊表示活動的網。AOE網是一個帶權的有向無環圖。其中:頂點表示事件(Event),邊表示活動(Activity),權值表示活動持續的時間。通常可用AOE網來估算工程的完成時間。

    由於整個工程只有一個開始點和一個完成點,在正常的情況(無環)下,網中只有一個入度爲零的點(稱作源點)和一個出度爲零的點(稱作匯點)

    依據AOE-網可以研究什麼問題?
(1)完成整項工程至少需要多少時間?
(2)哪些活動是影響工程進度的關鍵?

    完成工程的最短時間是從源點到匯點的最長路徑的長度。路徑長度最長的路徑叫做關鍵路徑
    假設開始點是v1,從v1到vi的最長路徑長度叫做事件vi的最早發生時間。這個時間決定了所有以vi爲尾的弧所表示的活動的最早開始時間。
    用e(i)表示活動ai的最早開始時間
    活動的最遲開始時間l(i),這是在不推遲整個工程完成的前提下,活動ai最遲必須開始進行的時間
    l(i)-e(i)兩者之差意味着完成活動ai的時間餘量。我們把l(i)=e(i)的活動叫做關鍵活動。
    顯然,關鍵路徑上的所有活動都是關鍵活動,因此提前完成非關鍵活動並不能加快工程的進度。 
    由此可知:辨別關鍵活動就是找e(i)=l(i)的活動。爲求得AOE網中活動的e(i)和l(i),首先應求得事件的最早發生時間 ve(j)和 最遲發生時間vl(j)。
 

    若活動ai由弧<i,j>表示,持續時間記爲dut(<i,j>),則有如下關係:
    活動i的最早開始時間等於事件i的最早發生時間:e(i)= ve(i)
    活動i的最遲開始時間等於事件j的最遲時間減去活動i的持續時間: l(i)= vl(j) - dut(<i,j>)
    求ve(j)和 vl(j)需分兩步進行:
ve[j]和vl[j]可以採用下面的遞推公式計算:
(1)向匯點遞推
    ve(源點) = 0 ;
    ve(j) = Max{ ve(i) + dut(<i, j>)}
公式意義:從指向頂點Vj的弧的活動中取最晚完成的一個活動的完成時間作爲Vj的最早發生時間ve[j]
2) 向源點遞推
     由上一步的遞推,最後總可求出匯點的最早發生時間ve[n]。因匯點就是結束點,最遲發生時間與最早發生時間相同,即vl[n]=ve[n]。從匯點最遲發生時間vl[n]開始,利用下面公式:
    vl(匯點) = ve(匯點);
    vl(i) = Min{ vl(j) – dut(<i, j>) }
公式意義:由從Vi頂點指出的弧所代表的活動中取最早開始的一個開始時間作爲Vi的最遲發生時間。 

2.算法描述

由此得到下述求關鍵路徑的算法:
1)輸入e條弧<i,j>,建立AOE網的存儲結構。
2)從源點v0出發,令ve[0]=0按拓撲有序求其餘各頂點的最早發生時ve[i](1≤i≤ n-1)。如果得到的拓撲有序序列中頂點個數小於網中頂點數n,則說明網中存在環,不能求關鍵路徑,算法終止;否則執行步驟(3)。
3)從匯點vn出發,令vl[n-1]= ve[n-1],按逆拓撲有序求其餘各頂點的最遲發生時間vl[i] (n-2 ≥i≥ 0);
4)根據各頂點的ve和vl值,求每條弧s的最早開始時間e(s)和最遲開始時間l(s)。若某條弧滿足條件e(s)=l(s),則爲關鍵活動

 

如上所述,計算頂點的ve值是在拓撲排序的過程中進行的,需對拓撲排序的算法作如下修改:
    1)在拓撲排序之前設初值,令ve(i)=0(0<=i<n-1);
    2)在算法中增加一個計算vi的直接後繼vj的最早發生時間的操作:若 ve(i)+dut(<i,j>) > ve(j), 則 ve(j) = ve(i)+dut(<i,j>);
    3)爲了能按逆拓撲有序序列的順序計算各頂點的vl值,需記下在拓撲排序的過程中求得的拓撲有序序列,則需要在拓撲排序算法中,增設一個棧以記錄拓撲有序序列,則在計算求得各頂點的 ve 值之後,從棧頂至棧底便爲逆拓撲有序序列。

示例:求下圖AOE網的關鍵路徑

AOE網中頂點事件和活動的發生時間:

總結:總之,關鍵路徑的求解操作包括:

1)計算 ve[j] 和 vl[j] 
 ① 向匯點遞推
 ve(源點) = 0 ;
 ve(j) = Max { ve(i)+ dut(<i, j>)}
 ② 向源點遞推
 vl(匯點) = ve(匯點);
 vl(i) = Min { vl(j) – dut(<i, j>)}
2)判斷 l(i) = e(i)的活動(關鍵活動)

3.代碼實現

3.1定義

 

/*DAG 有向無環圖的應用--關鍵路徑:能否順利完成工程,即檢查是否存在環(拓撲排序),如果無環,則求解整個工程完成所必須的最短時間
AOE網:邊表示活動的網,是一個帶權的DAG

  關鍵路徑即路徑長度最長的路徑
  即完成工程的最短時間是從開始點到完成點的最長路徑的長度(這裏所說的路徑長度是指各活動持續時間之和,不是路徑上弧的數目)
  關鍵活動:關鍵路徑上的所有活動,特點:最早開始時間=最遲開始時間
*/

//本示例依然以鄰接表作爲有向圖的存儲結構
/*
DAG 有向無環圖的應用--拓撲排序:能否順利完成工程,即檢查是否存在環,
AOV網:頂點表示活動的網

除了拓撲排序檢查環以外,還可以用DFS
當有向圖中無環時,從圖中某點進行深度優先遍歷時,最先退出DFS函數的頂點即出度爲0的頂點,是拓撲序列中的最後一個頂點,由此,按退出DFS函數的先後記錄下來的頂點序列,即爲逆向的拓撲有序序列
*/

//本次示例採用鄰接表作爲有向圖的存儲結構


#include<stdio.h>
#include<stdlib.h>
/*
圖的表示方法
DG(有向圖)或者DN(有向網):鄰接矩陣、鄰接表(逆鄰接表--爲求入度)、十字鏈表
UDG(無向圖)或者UDN(無向網):鄰接矩陣、鄰接表、鄰接多重表
*/
#define MAX_VERTEX_NUM 10//最大頂點數目
#define NULL 0
typedef int VRType;//對於帶權圖或網,則爲相應權值
typedef int VertexType;//頂點類型
//typedef enum GraphKind {DG, DN, UDG, UDN};  //有向圖:0,有向網:1,無向圖:2,無向

typedef struct ArcNode{	
	int adjvex;//該弧所指向的頂點的在圖中位置
	VRType w;//弧的相應權值
	struct ArcNode *nextarc;//指向下一條弧的指針
}ArcNode;//弧結點信息

typedef struct VNode{
	VertexType data;//頂點信息

	ArcNode *firstarc;//指向第一條依附該頂點的弧的指針
}VNode, AdjVexList[MAX_VERTEX_NUM];//頂點結點信息

typedef struct{
	AdjVexList vexs;//頂點向量
	int vexnum, arcnum;//圖的當前頂點數和弧數
	//GraphKind kind;//圖的種類標誌
}ALGraph;//鄰接表表示的圖

 

 

#define OK 1
#define ERROR 0
typedef int status;

int indegree[MAX_VERTEX_NUM] = {0};//存放各個頂點的入度的數組
int ve[MAX_VERTEX_NUM];//事件的最早發生時間
int vl[MAX_VERTEX_NUM];//事件的最遲發生時間


typedef struct{
	int s[MAX_VERTEX_NUM];
	int top;
}stack;

 

 

 

3.2鄰接表表示有向網

 

 

//若圖G中存在頂點v,則返回v在圖中的位置信息,否則返回其他信息
int locateVex(ALGraph G, VertexType v){
	for(int i = 0; i < G.vexnum; i++){
		if(G.vexs[i].data == v)
			return i;
	}
	return -1;//圖中沒有該頂點
}


//採用鄰接表表示法構造有向圖G
void createDG(ALGraph &G){
	printf("輸入頂點數和弧數如:(5,3):");
	scanf("%d,%d", &G.vexnum, &G.arcnum);

	//構造頂點向量,並初始化
	printf("輸入%d個頂點(以空格隔開如:v1 v2 v3):", G.vexnum);
	getchar();//吃掉換行符
	for(int m = 0; m < G.vexnum; m++){
		scanf("v%d", &G.vexs[m].data);
		G.vexs[m].firstarc = NULL;//初始化爲空指針////////////////重要!!!
		getchar();//吃掉空格符
	}

	//構造鄰接表
	VertexType v1, v2;//分別是一條弧的弧尾和弧頭(起點和終點)
	VRType w;//對於無權圖或網,用0或1表示相鄰否;對於帶權圖或網,則爲相應權值	
	printf("\n每行輸入一條弧依附的頂點(先弧尾後弧頭)和權值(如:v1 v2 3):\n");
	fflush(stdin);//清除殘餘後,後面再讀入時不會出錯
	int i = 0, j = 0;
	for(int k = 0; k < G.arcnum; k++){
		scanf("v%d v%d %d",&v1, &v2, &w);
		fflush(stdin);//清除殘餘後,後面再讀入時不會出錯
		i = locateVex(G, v1);//弧起點
		j = locateVex(G, v2);//弧終點
		
		//採用“頭插法”在各個頂點的弧鏈頭部插入弧結點
		ArcNode *p1 = (ArcNode *)malloc(sizeof(ArcNode));//構造一個弧結點,作爲弧vivj的弧頭(終點)
		p1->adjvex = j;
		p1->w = w;
		p1->nextarc = G.vexs[i].firstarc;
		G.vexs[i].firstarc = p1;
		/*因爲是有向圖,所以不必創建2個弧結點
		ArcNode *p2 = (ArcNode *)malloc(sizeof(ArcNode));//構造一個弧結點,作爲弧vivj的弧尾(起點)
		p2->adjvex = i;
		//p2->w = w;
		p2->nextarc = G.vexs[j].firstarc;
		G.vexs[j].firstarc = p2;
		*/
	}
}

 

3.3關鍵路徑求解算法的實現

 

改進的拓撲排序:

 

void findInDegree(ALGraph G, int indegree[]){	
	ArcNode *p;
	for(int i = 0; i < G.vexnum; i++){
		for(p = G.vexs[i].firstarc; p; p = p->nextarc){
			indegree[p->adjvex]++;
		}
	}	
}

//如有向圖無迴路,則產生G的頂點的一個拓撲序列並存到棧T中並返回OK,否則返回ERROR
//有向網G採用鄰接表做存儲結構,求解各個頂點事件的最早發生時間ve
status toplogicalOrder(ALGraph G, stack &T){
	//先初始化各個頂點的入度
	findInDegree(G, indegree);

	stack S;//維護一個棧來存放入度爲0的頂點,當棧爲空時,則說明圖中不存在無前驅的頂點了(即沒有入度爲0的頂點了),說明圖中無環
	S.top = 0;//否則如果此時仍然存在頂點,而且這些頂點有前驅,則說明有環

	//將入度爲0的頂點入棧
	for(int i = 0; i < G.vexnum; i++){
		if(!indegree[i]){
			S.s[S.top++] = i;
		}
	}
	//初始化事件的最早發生時間數組ve
	for(i = 0; i < G.vexnum; i++){
		ve[i] = 0;
	}

	int count = 0;//對輸出的頂點計數
	ArcNode *p;
	while(S.top != 0){//棧不爲空
		int topElemVex_i = S.s[--S.top];//棧頂元素出棧,即第一個無前驅的頂點
		//printf("v%d ", G.vexs[topElemVex_i].data);//輸出當前結點
		T.s[T.top++] = topElemVex_i;//入T棧,即爲拓撲序列中的一份子
		count++;

		//去掉以該結點爲前驅的點與他的弧,以將相關頂點的入度減1的操作來實現
		for(p = G.vexs[topElemVex_i].firstarc; p; p = p->nextarc){
			indegree[p->adjvex]--;
			if(!indegree[p->adjvex]){
				S.s[S.top++] = p->adjvex;//入度爲0者入棧
			}
			//出S棧的棧頂元素是拓撲序列當前訪問的結點,按拓撲序列,那麼接下來以他爲前驅的頂點的最早發生時間可能就會需要更新
			//更新頂點vi到v(p->adjvex),v(p->adjvex)的最早發生時間
			if(ve[topElemVex_i] + p->w > ve[p->adjvex]){
				ve[p->adjvex] = ve[topElemVex_i] + p->w ;
			}
		}
	}
	printf("\n");

	if(count < G.vexnum)//該有向圖有迴路
		return ERROR;
	else
		return OK;
}

求解關鍵活動:

 

 

//G爲有向網,輸出G的各項關鍵活動
status criticalPath(ALGraph G, stack T){
	if(!toplogicalOrder(G, T))
		return ERROR;
	
	//初始化事件的最遲開始時間數組vl
	for(int i = 0; i < G.vexnum; i++){
		vl[i] = ve[G.vexnum-1];//均初始化成匯點的最早發生時間
	}

	ArcNode *p;
	while(T.top != 0){//不爲空棧,棧T裏存放了拓撲序列,從棧頂到棧底爲拓撲逆序
		int topElemVex_i = T.s[--T.top];//棧頂元素出棧,按拓撲逆序出棧

		//按拓撲逆序求解各頂點的最遲開始時間
		for(p = G.vexs[topElemVex_i].firstarc; p ; p = p->nextarc){//p指向的頂點是topElemVex_i頂點的直接後繼
			if(vl[p->adjvex] - p->w < vl[topElemVex_i]){
				vl[topElemVex_i] = vl[p->adjvex] - p->w;
			}
		}
	}

	/*
	//test
	for(i = 0; i < G.vexnum; i++){
		printf("%d %d\n", ve[i], vl[i]);
	}
	*/




	printf("\n");
	//然後開始求解活動的最早開始時間和最遲開始時間,有幾個活動就有幾條邊,注意我們的存儲結構是鄰接表
	//所以依次訪問鄰接表中的每個頂點指着的弧鏈表,就可以訪問到所有的弧結點
	int ee;//活動的最早發生時間
	int el;//活動的最遲發生時間
	char tag;//表示是否是關鍵活動,'*'表示是關鍵活動
	for(i = 0; i < G.vexnum; i++){
		for(p = G.vexs[i].firstarc; p ; p = p->nextarc){//P指向每個弧節點
			//此時是弧:vi---v(p->adjvex),p指向vi的後繼,弧p對應的最早開始時間與最遲開始時間分別爲ee,el
			ee = ve[i];//ee爲活動前一時間的最早開始時間
			el = vl[p->adjvex] - p->w;//el= 活動後事件的最遲開始時間-活動持續時間
			tag = (ee == el) ? '*' : ' ';
			printf("v%dv%d:%d, 活動最早開始時間:%d,活動最晚開始時間:%d,%c\n", G.vexs[i].data, G.vexs[p->adjvex].data, p->w, ee, el, tag);//輸出活動的ee與el,標識有*號的代表關鍵活動
		}
	}
	printf("\n");
	return OK;
}

 

 

 

3.4演示

 

 

/*測試:
6,8
v1 v2 v3 v4 v5 v6

v1 v2 3
v1 v3 2

v2 v4 2
v2 v5 3

v3 v4 4
v3 v6 3

v4 v6 2
v5 v6 1
*/
void main(){
	ALGraph G;
	createDG(G);
	//printAdjList(G);
	
	stack T;//維護一個棧,用來存儲拓撲有序序列
	T.top = 0;

	//toplogicalOrder(G, T);
	/*
	printf("該圖的拓撲排序序列爲:");
	for(int i = 0; i < T.top; i++)//從棧底到棧頂是一個拓撲序
		printf("v%d ", G.vexs[T.s[i]]);
	printf("\n");
	*/

	criticalPath(G, T);			
}

 

 

 

 

 

總結:
    有向無環圖是描述一項工程或系統的進行過程的有效工具。
    AOV網(頂點表示活動的有向網)與拓撲排序--解決工程或系統能否順利進行;
    AOE網(邊表示活動的有向網)和關鍵路徑問題--估算整個工程完成所必須的最短時間,求解哪些活動爲關鍵活動。

    提高關鍵活動的速度,纔有可能加快整個工程的進度,提高非關鍵活動則是不可能加快整個工程的的。但是關鍵活動的速度提高是有限度的,只有在不改變網的關鍵路徑的情況下,提高關鍵活動的速度纔有效。另一方面,若網中有幾條關鍵路徑,那麼單單提高一條關鍵路徑上的關鍵活動的速度,還不能導致整個工程縮短工期,而必須提高同時在幾條關鍵路徑上的活動的速度。
 

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