【1】關鍵路徑
在我的經驗意識深處,“關鍵”二字一般都是指臨界點。
凡事萬物都遵循一個度的問題,那么存在度就會自然有臨界點。
關鍵路徑也正是研究這個臨界點的問題。
在學習關鍵路徑前,先了解一個AOV網和AOE網的概念:
用頂點表示活動,用弧表示活動間的優先關系的有向圖:
稱為頂點表示活動的網(Activity On Vertex Network),簡稱為AOV網。
與AOV網對應的是AOE(Activity On Edge)網即邊表示活動的網。
AOE網是一個帶權的有向無環圖。
網中只有一個入度為零的點(稱為源點)和一個出度為零的點(稱為匯點)。
其中,頂點表示事件(Event),弧表示活動,權表示活動持續的時間。
通常,AOE網可用來估算工程的完成時間。
假如汽車生產工廠要制造一輛汽車,制造過程的大概事件和活動時間如上圖AOE網:
我們把路徑上各個活動所持續的時間之和稱為路徑長度,從源點到匯點具有最大長度的路徑叫關鍵路徑,在關鍵路徑上的活動叫關鍵活動。
那么,顯然對上圖AOE網而言,所謂關鍵路徑:
開始-->發動機完成-->部件集中到位-->組裝完成。路徑長度為5.5。
如果我們試圖縮短整個工期,去改進輪子的生產效率,哪怕改動0.1也是無益的。
只有縮短關鍵路徑上的關鍵活動時間才可以減少整個工期的長度。
例如如果制造發動機縮短為2.5天,整車組裝縮短為1.5天,那么關鍵路徑為4.5。
工期也就整整縮短了一天時間。
好吧! 那么研究這個關鍵路徑意義何在?
假定上圖AOE網中弧的權值單位為小時,而且我們已經知道黑深色的那一條為關鍵路徑。
假定現在上午一點,對於外殼完成事件而言,為了不影響工期:
外殼完成活動最早也就是一點開始動工,最晚在兩點必須要開始動工。
最大權值3表示所有活動必須在三小時之后完成,而外殼完成只需要2個小時。
所以,這個中間的空閑時間有一個小時,為了不影響整個工期,它必須最遲兩點動工。
那么才可以保證3點時與發動機完成活動同時竣工,為后續的活動做好准備。
對AOE網有待研究的問題是:
(1)完成整個工程至少需要多少時間?
(2)那些活動是影響工程進度的關鍵?
今天研究是實例如下圖所示:
假想是一個有11項活動的AOE網,其中有9個事件(V1,V2,V3...V9)。
每個事件表示在它之前的活動已經完成,在它之后的活動可以開始。
如V1表示整個工程開始,V9表示整個共結束,V5表示a4和a5已經完成,a7和a8可以開始。
【2】關鍵路徑算法
為了更好的理解算法,我們先需要定義如下幾個參數:
(1)事件的最早發生時間etv(earliest time of vertex): 即頂點Vk的最早發生時間。
(2)事件的最晚發生時間ltv(latest time of vertex): 即頂點Vk的最晚發生時間。
也就是每個頂點對應的事件最晚需要開始的時間,超出此時間將會延誤整個工期。
(3)活動的最早開工時間ete(earliest time of edge): 即弧ak的最早發生時間。
(4)活動的最晚開工時間lte(latest time of edge): 即弧ak的最晚發生時間,也就是不推遲工期的最晚開工時間。
然后根據最早開工時間ete[k]和最晚開工時間lte[k]相等判斷ak是否是關鍵路徑。
將AOE網轉化為鄰接表結構如下圖所示:
與拓撲序列鄰接表結構不同的地方在於,弧鏈表增加了weight域,用來存儲弧的權值。
求事件的最早發生時間etv的過程,就是從頭至尾找拓撲序列的過程。
因此,在求關鍵路徑之前,先要調用一次拓撲序列算法的代碼來計算etv和拓撲序列表。
數組etv存儲事件最早發生時間
數組ltv存儲事件最遲發生時間
全局棧用來保存拓撲序列
注意代碼中的粗部分與原拓撲序列的算法區別。
第11-15行 初始化全局變量etv數組。
第21行 就是講要輸出的拓撲序列壓入全局棧。
第 27-28 行很關鍵,它是求etv數組的每一個元素的值。
比如:假如我們已經求得頂點V0的對應etv[0]=0;頂點V1對應etv[1]=3;頂點V2對應etv[2]=4
現在我們需要求頂點V3對應的etv[3],其實就是求etv[1]+len<V1,V3>與etv[2]+len<V2,V3>的較大值
顯然3+5<4+8,得到etv[3]=12,在代碼中e->weight就是當前弧的長度。如圖所示:
由此也可以得到計算頂點Vk即求etv[k]的最早發生時間公式如上。
下面具體分析關鍵路徑算法:
1. 程序開始執行。第5行,聲明了etv和lte兩個活動最早最晚發生時間變量
2. 第6行,調用求拓撲序列的函數。
執行完畢后,全局數組etv和棧的值如下所示796,也就是說已經確定每個事件的最早發生時間。
3. 第7-9行初始化數組ltv,因為etv[9]=27,所以數組當前每項均為27。
4. 第10-19行為計算ltv的循環。第12行,先將全局棧的棧頭出棧,由后進先出得到gettop=9。
但是,根據鄰接表中信息,V9沒有弧。所以至此退出循環。
5. 再次來到第12行,gettop=8,在第13-18行的循環中,V8的弧表只有一條<V8,V9>
第15行得到k=9,因為ltv[9]-3<ltv[8],所以ltv[8]=ltv[9]-3=24,過程如下圖所示:
6. 再次循環,當gettop=7,5,6時,同理可計算出ltv相對應的值為19,25,13。
此時ltv值為:{27,27,27,27,27,13,25,19,24,27}
7. 當gettop=4時,由鄰接表信息可得到V4有兩條弧<V4,V6>和<V4,V7>。
通過第13-18行的循環,可以得到ltv[4]=min(ltv[7]-4,ltv[6]-9)=min(19-4,25-9)=15
過程分析如下圖所示:
當程序執行到第20行時,相關變量的值如下圖所示。
比如etv[1]=3而ltv[1]=7表示(如果單位按天計的話):
哪怕V1這個事件在第7天才開始也是可以保證整個工程按期完成。
你也可以提前V1時間開始,但是最早也只能在第3天開始。
8. 第20-31行是求另兩個變量活動最早開始時間ete和活動最晚時間lte。
當 j=0 時,從V0頂點開始,有<V0,V2>和<V0,V1>兩條弧。
當 k=2 時,ete=etv[j]=etv[0]=0
lte=ltv[k]-e->weight=ltv[2]-len<v0,v2>=4-4=0 此時ete == lte
表示弧<v0,v2>是關鍵活動,因此打印。
當 k=1 時,ete=etv[j]=etv[0]=0
lte=ltv[k]-e->weight=ltv[2]-len<v0,v1>=7-3=4 此時ete != lte
表示弧<v0,v1>並不是關鍵活動。如圖所示:
說明:ete表示活動<Vk,Vj>的最早開工時間,是針對弧來說的。
但是只有此弧的弧尾頂點Vk的事件發生了,它才可以開始,ete=etv[k]。
lte表示的是活動<Vk,Vj>最晚開工時間,但此活動再晚也不能等V1事件發生才開始。
而必須要在V1事件之前發生,所以lte=ltv[j]-len<Vk,Vj>。
9. j=1 直到 j=9 為止,做法完全相同。
最終關鍵路徑如下圖所示:
注意:本例是唯一一條關鍵路徑,並不等於不存在多條關鍵路徑。
如果是多條關鍵路徑,則單是提高一條關鍵路徑上的關鍵活動速度並不是能導致整個工程縮短工期、
而必須提高同時在幾條關鍵路徑上的活動的速度。
【3】關鍵路徑是代碼實現
本示例代碼與算法有些不同,但是效果相同,都是為了達到一個共同目的:理解並學習關鍵路徑算法。

1 #include <iostream>
2 #include "Stack.h"
3 #include <malloc.h>
4 using namespace std; 5
6 #define MAXVEX 10
7 #define MAXEDGE 13
8
9 // 全局棧
10 SeqStack<int> sQ2; 11
12 typedef struct EdgeNode 13 { 14 int adjvex; // 鄰接點域,存儲該頂點對應的下標
15 int weight; // 邊的權值
16 struct EdgeNode* next; // 鏈域
17 } EdgeNode; 18
19 typedef struct VertexNode 20 { 21 int inNum; // 頂點入度值
22 int data; // 頂點數值欲
23 EdgeNode* firstedge; // 邊表頭指針
24 } VertexNode, AdjList[MAXVEX]; 25
26 typedef struct
27 { 28 AdjList adjList; 29 int numVertexes, numEdges; // 圖中當前頂點數和邊數(對於本案例,已經存在宏定義)
30 } graphAdjList, *GraphAdjList; 31
32 // 構建節點
33 EdgeNode* BuyNode() 34 { 35 EdgeNode* p = (EdgeNode*)malloc(sizeof(EdgeNode)); 36 p->adjvex = -1; 37 p->next = NULL; 38 return p; 39 } 40 // 初始化圖
41 void InitGraph(graphAdjList& g) 42 { 43 for (int i = 0; i < MAXVEX; ++i) 44 { 45 g.adjList[i].firstedge = NULL; 46 } 47 } 48 // 創建圖
49 void CreateGraph(graphAdjList& g) 50 { 51 int i = 0, begin = 0, end = 0, weight = 0; 52 EdgeNode *pNode = NULL; 53 cout << "輸入10個頂點信息(頂點 入度):" << endl; 54 for (i = 0; i < MAXVEX; ++i) 55 { 56 cin >> g.adjList[i].data >> g.adjList[i].inNum; 57 } 58 cout << "輸入13條弧的信息(起點 終點 權值):" << endl; 59 for (i = 0; i < MAXEDGE; ++i) 60 { 61 cin >> begin >> end >> weight; 62 pNode = BuyNode(); 63 pNode->adjvex = end; 64 pNode->weight = weight; 65 pNode->next = g.adjList[begin].firstedge; 66 g.adjList[begin].firstedge = pNode; 67 } 68 } 69 // 打印輸入信息的邏輯圖
70 void PrintGraph(graphAdjList &g) 71 { 72 cout << "打印AOE網的鄰接表邏輯圖:" << endl; 73 for (int i = 0; i < MAXVEX; ++i) 74 { 75 cout << " " << g.adjList[i].inNum << " " << g.adjList[i].data << " "; 76 EdgeNode* p = g.adjList[i].firstedge; 77 cout << "-->"; 78 while (p != NULL) 79 { 80 int index = p->adjvex; 81 cout << "[" << g.adjList[index].data << " " << p->weight << "] " ; 82 p = p->next; 83 } 84 cout << endl; 85 } 86 } 87 // 求拓撲序列
88 bool TopologicalSort(graphAdjList g, int* pEtv) 89 { 90 EdgeNode* pNode = NULL; 91 int i = 0, k = 0, gettop = 0; 92 int nCnt = 0; 93 SeqStack<int> sQ1; 94 for (i = 0; i < MAXVEX; ++i) 95 { 96 if (0 == g.adjList[i].inNum) 97 sQ1.Push(i); 98 } 99 for (i = 0; i < MAXVEX; ++i) 100 { 101 pEtv[i] = 0; 102 } 103 while (!sQ1.IsEmpty()) 104 { 105 sQ1.Pop(gettop); 106 ++nCnt; 107 sQ2.Push(gettop); // 將彈出的頂點序號壓入拓撲序列的棧
108 if (MAXVEX == nCnt) 109 { //去掉拓撲路徑后面的-->
110 cout << g.adjList[gettop].data << endl; 111 break; 112 } 113 cout << g.adjList[gettop].data << "-->"; 114 pNode = g.adjList[gettop].firstedge; 115 while (pNode != NULL) 116 { 117 k = pNode->adjvex; 118 --g.adjList[k].inNum; 119 if (0 == g.adjList[k].inNum) 120 sQ1.Push(k); 121 if (pEtv[gettop] + pNode->weight > pEtv[k]) 122 pEtv[k] = pEtv[gettop] + pNode->weight; 123 pNode = pNode->next; 124 } 125 } 126 return nCnt != MAXVEX; 127 } 128 // 關鍵路徑
129 void CriticalPath(graphAdjList g, int* pEtv, int* pLtv) 130 { 131 // pEtv 事件最早發生時間 132 // PLtv 事件最遲發生時間
133 EdgeNode* pNode = NULL; 134 int i = 0, gettop = 0, k =0, j = 0; 135 int ete = 0, lte = 0; // 聲明活動最早發生時間和最遲發生時間變量
136 for (i = 0; i < MAXVEX; ++i) 137 { 138 pLtv[i] = pEtv[MAXVEX-1]; // 初始化
139 } 140 while (!sQ2.IsEmpty()) 141 { 142 sQ2.Pop(gettop); // 將拓撲序列出棧,后進先出
143 pNode = g.adjList[gettop].firstedge; 144 while (pNode != NULL) 145 { // 求各頂點事件的最遲發生時間pLtv值
146 k = pNode->adjvex; 147 if (pLtv[k] - pNode->weight < pLtv[gettop]) 148 pLtv[gettop] = pLtv[k] - pNode->weight; 149 pNode = pNode->next; 150 } 151 } 152 // 求 ete, lte, 和 關鍵路徑
153 for (j = 0; j < MAXVEX; ++j) 154 { 155 pNode = g.adjList[j].firstedge; 156 while (pNode != NULL) 157 { 158 k = pNode->adjvex; 159 ete = pEtv[j]; // 活動最早發生時間
160 lte = pLtv[k] - pNode->weight; // 活動最遲發生時間
161 if (ete == lte) 162 cout << "<V" << g.adjList[j].data << ",V" << g.adjList[k].data << "> :" << pNode->weight << endl; 163 pNode = pNode->next; 164 } 165 } 166 } 167 void main() 168 { 169 graphAdjList myg; 170 InitGraph(myg); 171 cout << "創建圖:" << endl; 172 CreateGraph(myg); 173 cout << "打印圖的鄰接表邏輯結構:" << endl; 174 PrintGraph(myg); 175
176 int* pEtv = new int[MAXVEX]; 177 int* pLtv = new int[MAXVEX]; 178
179 cout << "求拓撲序列(全局棧sQ2的值):" << endl; 180 TopologicalSort(myg, pEtv); 181 cout << "打印數組pEtv(各個事件的最早發生時間):" << endl; 182 for(int i = 0; i < MAXVEX; ++i) 183 { 184 cout << pEtv[i] << " "; 185 } 186 cout << endl << "關鍵路徑:" << endl; 187
188 CriticalPath(myg, pEtv, pLtv); 189 cout << endl; 190 } 191 /*
192 創建圖: 193 輸入10個頂點信息(頂點 入度): 194 0 0 195 1 1 196 2 1 197 3 2 198 4 2 199 5 1 200 6 1 201 7 2 202 8 1 203 9 2 204 輸入13條弧的信息(起點 終點 權值): 205 0 1 3 206 0 2 4 207 1 3 5 208 1 4 6 209 2 3 8 210 2 5 7 211 3 4 3 212 4 6 9 213 4 7 4 214 5 7 6 215 6 9 2 216 7 8 5 217 8 9 3 218 打印圖的鄰接表邏輯結構: 219 打印AOE網的鄰接表邏輯圖: 220 0 0 -->[2 4] [1 3] 221 1 1 -->[4 6] [3 5] 222 1 2 -->[5 7] [3 8] 223 2 3 -->[4 3] 224 2 4 -->[7 4] [6 9] 225 1 5 -->[7 6] 226 1 6 -->[9 2] 227 2 7 -->[8 5] 228 1 8 -->[9 3] 229 2 9 --> 230 求拓撲序列(全局棧sQ2的值): 231 0-->1-->2-->3-->4-->6-->5-->7-->8-->9 232 打印數組pEtv(各個事件的最早發生時間): 233 0 3 4 12 15 11 24 19 24 27 234 關鍵路徑: 235 <V0,V2> :4 236 <V2,V3> :8 237 <V3,V4> :3 238 <V4,V7> :4 239 <V7,V8> :5 240 <V8,V9> :3 241 */
本示例代碼中的Stack.h頭文件從隨筆《棧》中拷貝即可。
Good Good Study, Day Day Up.
順序 選擇 循環 總結