參考書籍:數據結構(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網(邊表示活動的有向網)和關鍵路徑問題--估算整個工程完成所必須的最短時間,求解哪些活動爲關鍵活動。
提高關鍵活動的速度,纔有可能加快整個工程的進度,提高非關鍵活動則是不可能加快整個工程的的。但是關鍵活動的速度提高是有限度的,只有在不改變網的關鍵路徑的情況下,提高關鍵活動的速度纔有效。另一方面,若網中有幾條關鍵路徑,那麼單單提高一條關鍵路徑上的關鍵活動的速度,還不能導致整個工程縮短工期,而必須提高同時在幾條關鍵路徑上的活動的速度。