AOV網和AOE網
AOV網
頂點活動網絡(Activity On Vertex, AOV):用頂點表示活動,邊集表示活動優先關系的有向圖。
上圖中,結點表示課程,有向邊表示課程的先導關系。
顯然,圖中不應該出現有向環,否則會讓優先關系出現邏輯錯誤。
AOE網
定義
邊活動網絡(Acitivity On Edge, AOE):用帶權的邊集表示活動,用頂點表示事件的有向圖。AOE比AOV包含更多信息。
上圖中,a1-a6表示活動,即要學習的課程;邊權表示學習時間;結點表示事件,如v2表示空間解析幾何已經學完,可以開始學習復變函數,v5表示泛函分析的先導課程已經學完,接下來可以開始學習泛函分析。
所以: “事件” 僅表示一個 中介狀態 。
顯然,圖中不應該出現有向環,否則會讓優先關系出現邏輯錯誤。
與AOV網的轉化
- AOV網中的頂點:拆分為兩個頂點,分別表示活動的起點和終點(在AOE網中就是兩個事件),而兩個頂點間用有向邊連接,該有向邊就表示AOV網中原頂點的活動,最后在賦予邊權。
- AOV網中的邊:原AOV網中的邊全部視為空活動,邊權為0。
AOE網中着重解決的兩個問題
AOE網是基於工程提出的概念,它着重解決兩個問題
1.最長路徑問題
工程起始到終止至少需要多少時間 取決於AOE網中的最長路徑。
如何理解此處的“至少”和“最長”? 設想你有一排手辦,而你要定制一個展示盒把所有的手辦全部裝在里面展示,那么,你所有定制的展示盒的最低高度顯然就取決於你那一排手辦里最高高度的手辦。也可以從反面理解,木桶平放時所能裝下的最高水位取決於木桶的最低板長。
2.關鍵活動問題
哪條(些)路徑上的活動是影響整個工程進度的關鍵:顯然最長路徑上的活動是影響整個工程進度的關鍵,如果縮短了最長路徑上活動的時間,就能縮短工程的總體時間,反之,就會延長。比如,如果一排手辦中最高的手辦的高度由1m變成0.5m,那么所要定制的展示盒就只需要0.5m高而不在是1m高。
總結
所以:我們稱最長路徑為“關鍵路徑”,稱最長路徑上的活動為“關鍵活動”。它們是影響工程時間的關鍵。
如果我們求出了關鍵路徑,就能求出工程最短時間。
而需要求出工程最短時間,必然要借助關鍵路徑。
最長路徑
無正環的圖
如果一個圖中沒有正環(指從原點可達的正環),那么只需要把邊權乘以-1,令其變成相反數,就可以將最長路徑問題轉化為最短路徑問題,使用Bellman-Ford或SPFA解決,注意不能使用Dijstra(無法處理負權邊)。
最短路徑問題介紹:<數據結構>圖的最短路徑問題
有向無環圖的最短路徑
見下文“關鍵路徑算法”
其他情況
為NP-Hard問題(無法用 多項式時間復雜度的算法解決 的問題)。
關鍵路徑算法:確定關鍵活動,求出工程最短時間
前置定義
e[a]: 活動a的最早發生時間
l[a]: 活動a的最晚發生時間
若 "e[a] == l[a]" 說明活動a不能拖延,活動a是關鍵活動。
ve[i]: 事件i的最早發生時間
le[i]: 事件i的最晚發生時間
求解e[a]、l[a]轉化為求解ve[i]、le[i]這兩個新數組
- 對於活動ar來說,只要在事件Vi發生是馬上開始,就可以使得活動ar開始的時間最早,因此e[r] = ve[i]
- 如果l[r]是活動ar的最遲發生時間,那么l[r] + length[r]就是事件Vj的最遲發生時間(length[r]表示活動a的邊權,即活動a持續時間)。因此l[r] = vl[j] - length[r]。
下面討論如何求解ve[ ]與vl[ ]
ve數組求解
數學分析
有k個事件 Vi1 ~ Vik,通過相應的活動ar1 ~ ark, 到達事件Vj。(如下圖)
活動的邊權分別為length[r1] ~ length[rk]。
假設 Vi1 ~ Vik 時間的最早發生時間 ve[i1] ~ ve[ik] 以及 length[r1] ~ length[rk] 均已知, 則 事件Vj發生的最早時間就是ve[i1]+length[r1] ~ ve[ik]+length[rk] 中的 最大值
此處時間節點Ve[]取最大值 是因為只有取最大值才能保證,在Vj開始時,Vj的所有先導事件都已完成。
代碼實現:拓撲排序
拓撲排序介紹:<數據結構>拓撲排序
-
根據上文分析,要想知道ve[j],那么ve[i1]~ve[ik]必須得到。 即在訪問某個結點時保證它的前驅結點都已經被訪問過 ————> 拓撲排序
-
在拓撲排序中無法根據當前結點得到它的前驅結點————>訪問到某個結點Vi時,不去尋找它的前驅結點,而是用它更新所有后繼結點的ve[]值,這樣,在訪問它的后繼結點時,后繼結點的ve值必然是已經被更新過的。
stack<int> topOrder; //拓撲序列,為后面的逆拓撲排序做准備
//拓撲排序,順便求ve數組。 ve數組初始化為0
bool topologicalSort(){
queue<int> q;
for(int i = 0; i<n; i++){
if(inDegree[i] == 0){
q.push(i);
}
}
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u); //將u加入拓撲序列
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的i號后繼結點的編號為v
inDegree[v]--;
if(inDegree[v] == 0){
q.push(v);
}
//用u來更新u的所有后繼結點v
if(ve[u]+G[u][i].w > ve[v]){
ve[v] = ve[u] + G[u][i].w;
}
}
}
if(topOrder.size() == n) return true;
else return false;
}
vl數組求解
數學分析
從事件Vi出發,通過相應的活動 ar1 ~ ark 可以到達k個事件 Vj1 ~ Vjk ,活動的邊權為 length[rl] ~ length[rk]。
假設 Vj1 ~ Vjk 時間的最遲發生時間 vl[j1] ~ vl[jk] 以及 length[r1] ~ length[rk] 均已知, 則 事件Vi發生的最遲時間就是vl[j1]-length[r1] ~ vl[jk]-length[rk] 中的 最小值
此處時間節點Vl[ik]取最小值 是因為只有取最小值才能保證,在Vi開始時,Vi的所有后繼事件都能(在其最晚時間節點前)完成。
代碼實現:逆拓撲排序
算出vl[i]需保證i的后繼結點的最遲時間即 vl[j1] ~ vl[jk] 都已被算出。
與求ve數組的過程反向,即將拓撲排序序列逆序訪問,同時更新vl值即可。
fill(vl,vl+n, ve[n-1]); //vl數組初始化,初始值為匯點的ve值
//直接使用topOrder出棧即為逆拓撲序列,求解vl數組
while(!topOrder.empty()){
int u = topOrder.top(); //棧頂元素為u
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的后繼結點v
//用u的所有后繼結點v的vl值來更新vl[u]
if(vl[v] - G[u][i].w < vl[u]){
vl[u] = vl[u] - G[u][i].w;
}
}
}
關鍵路徑算法實現
基本步驟
先求點,再夾邊:按下面三個步驟進行
主體代碼
適用於匯點唯一且確定 的情況,以n-1號頂點為匯點為例。
#include<stdio.h>
#include<vector>
#include<queue>
#include<stack>
#include<algorithm>
using namespace std;
const int MAXV = 100;
struct Node{
int v, w;
};
int ve[MAXV];
int vl[MAXV];
int n; //頂點數
int inDegree[MAXV]; //儲存結點入度,在主函數中初始化
vector<Node> G[MAXV]; //鄰接表表示圖G
stack<int> topOrder; //拓撲序列,為后面的逆拓撲排序做准備
bool topologicalSort(); //拓撲排序,計算ve數組(對應步驟1)
int CirticalPath(); //逆拓撲排序 與 輸出關鍵活動和換件路徑長度(對應步驟2、3)
//關鍵路徑,不是有向無環圖返回-1,否則返回關鍵路徑長度
int CirticalPath(){
fill(ve, ve+n, 0); //ve數組初始化為0,則匯點的ve值(0+關鍵路徑長度)就等於關鍵路徑長度。
if(topologicalSort() == false){ //調用拓撲排序函數,計算ve數組
return -1; //不是有向無環圖,返回-1
}
//此時的ve數組以經過拓撲排序已更新賦值,ve[n-1]為拓撲排序終點(即有向圖匯點)的ve值
fill(vl,vl+n, ve[n-1]); //vl數組初始化,初始值為匯點的ve值
//直接使用topOrder出棧即為 逆拓撲序列 ,求解vl數組
while(!topOrder.empty()){
int u = topOrder.top(); //棧頂元素為u
topOrder.pop();
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的后繼結點v
//用u的所有后繼結點v的vl值來更新vl[u]
if(vl[v] - G[u][i].w < vl[u]){
vl[u] = vl[u] - G[u][i].w;
}
}
}
//遍歷鄰接表的所有邊,計算活動的最早開始時間e和最遲開始時間l
for(int u = 0; u < n; u++){
for(int i = 0; i < G[u].size(); i++){
int v = G[u][v].v, w = G[u][v].w;
//活動的最早開始時間e和最遲開始時間l
int e = ve[u], l = vl[v] - G[u][v].w;
//如果e==l,說明活動u->v是關鍵路徑
if(e == l){
printf("%d->%d\n", u, v);
}
}
}
return ve[n-1]; // 返回關鍵路徑長度
}
//拓撲排序,順便求ve數組,ve數組初始化為0
bool topologicalSort(){
queue<int> q;
for(int i = 0; i<n; i++){
if(inDegree[i] == 0){
q.push(i);
}
}
while(!q.empty()){
int u = q.front();
q.pop();
topOrder.push(u); //將u加入拓撲序列
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v; //u的i號后繼結點的編號為v
inDegree[v]--;
if(inDegree[v] == 0){
q.push(v);
}
//用u來更新u的所有后繼結點v
if(ve[u]+G[u][i].w > ve[v]){
ve[v] = ve[u] + G[u][i].w;
}
}
}
if(topOrder.size() == n) return true; //無環,可計算關鍵路徑
else return false; //圖中有環,無法計算關鍵路徑
}
幾個注意點
- 圖采用鄰接表示實現:<數據結構>圖的構建與基本遍歷方法
- e和l只是用來判斷關鍵活動,並輸出,不必保存。如果需要保存可在結構體Node中添加域e和l。
- 范圍拓展:
a.匯點不唯一:引入超級匯點,將所有匯點指向超級匯點,再調用函數CritialPath()
b.匯點不確定:尋找匯點,匯點的ve值必然最大,故可在 fill函數之前找到ve數組最大值即可
替換為:fill(vl, vl+n, v[n-1])
int maxLength = 0; for(int i = 0; i < n; i++){ if(ve[i] > manLength){ maxLength = ve[i]; } }