AOE網:關鍵路徑和關鍵活動
阿新 • • 發佈:2018-11-09
關鍵路徑
在我的經驗意識深處,“關鍵”二字一般都是指臨界點。
凡事萬物都遵循一個度的問題,那麼存在度就會自然有臨界點。
關鍵路徑也正是研究這個臨界點的問題。
在學習關鍵路徑前,先了解一個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】關鍵路徑是程式碼實現
本示例程式碼與演算法有些不同,但是效果相同,都是為了達到一個共同目的:理解並學習關鍵路徑演算法。
#include <iostream>
#include "Stack.h"
#include <malloc.h>
using namespace std;
#define MAXVEX 10
#define MAXEDGE 13
// 全域性棧
SeqStack<int> sQ2;
typedef struct EdgeNode
{
int adjvex; // 鄰接點域,儲存該頂點對應的下標
int weight; // 邊的權值
struct EdgeNode* next; // 鏈域
} EdgeNode;
typedef struct VertexNode
{
int inNum; // 頂點入度值
int data; // 頂點數值欲
EdgeNode* firstedge; // 邊表頭指標
} VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes, numEdges; // 圖中當前頂點數和邊數(對於本案例,已經存在巨集定義)
} graphAdjList, *GraphAdjList;
// 構建節點
EdgeNode* BuyNode()
{
EdgeNode* p = (EdgeNode*)malloc(sizeof(EdgeNode));
p->adjvex = -1;
p->next = NULL;
return p;
}
// 初始化圖
void InitGraph(graphAdjList& g)
{
for (int i = 0; i < MAXVEX; ++i)
{
g.adjList[i].firstedge = NULL;
}
}
// 建立圖
void CreateGraph(graphAdjList& g)
{
int i = 0, begin = 0, end = 0, weight = 0;
EdgeNode *pNode = NULL;
cout << "輸入10個頂點資訊(頂點 入度):" << endl;
for (i = 0; i < MAXVEX; ++i)
{
cin >> g.adjList[i].data >> g.adjList[i].inNum;
}
cout << "輸入13條弧的資訊(起點 終點 權值):" << endl;
for (i = 0; i < MAXEDGE; ++i)
{
cin >> begin >> end >> weight;
pNode = BuyNode();
pNode->adjvex = end;
pNode->weight = weight;
pNode->next = g.adjList[begin].firstedge;
g.adjList[begin].firstedge = pNode;
}
}
// 列印輸入資訊的邏輯圖
void PrintGraph(graphAdjList &g)
{
cout << "列印AOE網的鄰接表邏輯圖:" << endl;
for (int i = 0; i < MAXVEX; ++i)
{
cout << " " << g.adjList[i].inNum << " " << g.adjList[i].data << " ";
EdgeNode* p = g.adjList[i].firstedge;
cout << "-->";
while (p != NULL)
{
int index = p->adjvex;
cout << "[" << g.adjList[index].data << " " << p->weight << "] " ;
p = p->next;
}
cout << endl;
}
}
// 求拓撲序列
bool TopologicalSort(graphAdjList g, int* pEtv)
{
EdgeNode* pNode = NULL;
int i = 0, k = 0, gettop = 0;
int nCnt = 0;
SeqStack<int> sQ1;
for (i = 0; i < MAXVEX; ++i)
{
if (0 == g.adjList[i].inNum)
sQ1.Push(i);
}
for (i = 0; i < MAXVEX; ++i)
{
pEtv[i] = 0;
}
while (!sQ1.IsEmpty())
{
sQ1.Pop(gettop);
++nCnt;
sQ2.Push(gettop); // 將彈出的頂點序號壓入拓撲序列的棧
if (MAXVEX == nCnt)
{ //去掉拓撲路徑後面的-->
cout << g.adjList[gettop].data << endl;
break;
}
cout << g.adjList[gettop].data << "-->";
pNode = g.adjList[gettop].firstedge;
while (pNode != NULL)
{
k = pNode->adjvex;
--g.adjList[k].inNum;
if (0 == g.adjList[k].inNum)
sQ1.Push(k);
if (pEtv[gettop] + pNode->weight > pEtv[k])
pEtv[k] = pEtv[gettop] + pNode->weight;
pNode = pNode->next;
}
}
return nCnt != MAXVEX;
}
// 關鍵路徑
void CriticalPath(graphAdjList g, int* pEtv, int* pLtv)
{
// pEtv 事件最早發生時間
// PLtv 事件最遲發生時間
EdgeNode* pNode = NULL;
int i = 0, gettop = 0, k =0, j = 0;
int ete = 0, lte = 0; // 宣告活動最早發生時間和最遲發生時間變數
for (i = 0; i < MAXVEX; ++i)
{
pLtv[i] = pEtv[MAXVEX-1]; // 初始化
}
while (!sQ2.IsEmpty())
{
sQ2.Pop(gettop); // 將拓撲序列出棧,後進先出
pNode = g.adjList[gettop].firstedge;
while (pNode != NULL)
{ // 求各頂點事件的最遲發生時間pLtv值
k = pNode->adjvex;
if (pLtv[k] - pNode->weight < pLtv[gettop])
pLtv[gettop] = pLtv[k] - pNode->weight;
pNode = pNode->next;
}
}
// 求 ete, lte, 和 關鍵路徑
for (j = 0; j < MAXVEX; ++j)
{
pNode = g.adjList[j].firstedge;
while (pNode != NULL)
{
k = pNode->adjvex;
ete = pEtv[j]; // 活動最早發生時間
lte = pLtv[k] - pNode->weight; // 活動最遲發生時間
if (ete == lte)
cout << "<V" << g.adjList[j].data << ",V" << g.adjList[k].data << "> :" << pNode->weight << endl;
pNode = pNode->next;
}
}
}
void main()
{
graphAdjList myg;
InitGraph(myg);
cout << "建立圖:" << endl;
CreateGraph(myg);
cout << "列印圖的鄰接表邏輯結構:" << endl;
PrintGraph(myg);
int* pEtv = new int[MAXVEX];
int* pLtv = new int[MAXVEX];
cout << "求拓撲序列(全域性棧sQ2的值):" << endl;
TopologicalSort(myg, pEtv);
cout << "列印陣列pEtv(各個事件的最早發生時間):" << endl;
for(int i = 0; i < MAXVEX; ++i)
{
cout << pEtv[i] << " ";
}
cout << endl << "關鍵路徑:" << endl;
CriticalPath(myg, pEtv, pLtv);
cout << endl;
}
/*
建立圖:
輸入10個頂點資訊(頂點 入度):
0 0
1 1
2 1
3 2
4 2
5 1
6 1
7 2
8 1
9 2
輸入13條弧的資訊(起點 終點 權值):
0 1 3
0 2 4
1 3 5
1 4 6
2 3 8
2 5 7
3 4 3
4 6 9
4 7 4
5 7 6
6 9 2
7 8 5
8 9 3
列印圖的鄰接表邏輯結構:
列印AOE網的鄰接表邏輯圖:
0 0 -->[2 4] [1 3]
1 1 -->[4 6] [3 5]
1 2 -->[5 7] [3 8]
2 3 -->[4 3]
2 4 -->[7 4] [6 9]
1 5 -->[7 6]
1 6 -->[9 2]
2 7 -->[8 5]
1 8 -->[9 3]
2 9 -->
求拓撲序列(全域性棧sQ2的值):
0-->1-->2-->3-->4-->6-->5-->7-->8-->9
列印陣列pEtv(各個事件的最早發生時間):
0 3 4 12 15 11 24 19 24 27
關鍵路徑:
<V0,V2> :4
<V2,V3> :8
<V3,V4> :3
<V4,V7> :4
<V7,V8> :5
<V8,V9> :3
*/