引入
AOE網和AOV網
上一篇的拓撲排序中提到了AOV網(Activity On Vertex Network),與之相對應的是AOE網(Activity on edge network),即邊表示活動的網。
AOV用頂點表示活動的網,描述活動之間的制約關系。

AOE是帶權值的有向圖,以頂點表示事件,以邊表示活動,邊上的權值表示活動的開銷(如項目工期)。AOE 是建立在子過程之間的制約關系沒有矛盾的基礎之上,再來分析整個過程需要的開銷。所以如果給定AOV網中各頂點活動所需要的時間,則可以轉換為AOE網,較為簡單的方法就是把每個頂點都拆分成兩個頂點,分別表示活動的起點和終點

事件和活動
把上圖轉換成一般的AOE圖如下

“活動”表示學習課程的過程,而“事件”表示的是一個時間點或者說一種狀態(自己的理解),開始和完成活動是一個事件,比如:V4表示學完C語言。而這個事件同時也代表這前面的課程都已經學完了,可以開始學后面的課程了。
關鍵路徑
AOE一般用來估算工程的完成時間。AOE表示工程的流程,把沒有入邊的稱為始點或者源點,沒有出邊的頂點稱為終點或者匯點。一般情況下,AOE只有一個源點一個匯點。但上面用的例圖就不止一個,如果碰到這種情況,就可以再加一個“超級源(終)點”,連接所有入(出)度為0的點(不加也不會影響最后的答案)。
關鍵路徑:從源點到匯點具有最長路徑(強調:就是AOE網中權值和最大的路徑),在關鍵路徑上的活動叫關鍵活動。但為什么是最大長度呢?
關鍵路徑是AOE網中的最長路徑,也是整個工程的最短完成時間,如何理解此處的“最長”和“最短”呢?比如我們想要把“算法設計分析”學完,那么需要的時間就是$max(A1,A2)+(A4)+(A7) = 105days$,那么這105days是最長路徑,也是整個工程的最短完成時間,如果我們試圖縮短學習的時間,那么縮短“C語言”課程的學習時間顯然是沒有用的。只有縮短關鍵路徑上的關鍵活動時間才可以減少整個項目的時間。比如讓“離散數學”的時間縮短為30days,則總時間就會減少為90days。
來看四個定義(活動是一個過程,用“開始”,事件是一個時間點,用“發生”):
活動的最早開始時間 ETE(earliest time of edge):所有前導活動都完成,可以開始的時間。
活動的最晚開始時間 LTE(latest time of edge):不推遲工期的最晚開工時間。
事件的最早發生時間 ETV(earliest time of vertex):可以等價理解為舊活動的最早結束時間 或 新活動的最早開始時間
事件的最晚發生時間 LTV(latest time of vertex):可以等價理解為就活動的最晚結束時間 或 新活動的最晚開始時間
舉例說明一下,“數據結構”課程的活動最早開始時間就是“離散數學”學完,45days。對於“C語言”課程來說,需要30days,而“離散數學”需要45days,那么“C語言”在“離散數學”開始后的15days,再開始也不會延遲整個學習的時間,這就是活動最晚開始時間。
算法描述
我們把關鍵路徑上的活動稱作關鍵活動,那么對於關鍵活動來說,它們是不允許拖延的,因此這些活動的最早開始時間必須是等於最晚開始時間,同理,把關鍵路徑上的事件稱作關鍵事件,他們的最早發生時間也是等於最晚發生時間。因此可以設置數組$E$和$L$,其中$E[r]$和$L[r]$分別表示活動$A_r$的最早開始時間和最晚開始時間,於是,我們只要求出這兩個數組就可以通過判斷$E[r]==L[r]$來確定$r$是否為關鍵活動了。
再引入兩個新的數組$VE$和$VL$,其中$VE[i]$和$VL[i]$分別表示事件$i$的最早發生時間和最晚發生時間。
舉個例子,看下圖

我們可以得出以下四個等式
1.事件$V_i$的最早發生時間就是活動$A_r$的最早開始時間,即$E[r]=VE[i]$
2.事件$V_j$的最早發生時間就是活動$A_r$的最早開始時間$+$活動$A_r$的權值,即$E[r]+length[r]=VE[j]$
3.事件$V_i$的最晚發生時間就是活動$A_r$的最晚開始時間,即$VL[i]=L[r]$
4.事件$V_j$的最晚發生時間就是活動$A_r$的最晚開始時間$+$活動$A_r$的權值,即$VL[j]=L[r]+length[r]$
把1、2合起來就是$VE[j]=VE[i]+length[r]$,把3、4合起來就是$VL[i]=VL[j]-length[r]$,這樣我們就可以先要求出$VE$和$VL$這兩個數組,然后通過上面的等式得到$E$和$L$數組。
求VE數組
根據$VE[j]=VE[i]+length[r]$,假設我們已知了事件$V_{i1},...V_{ik}$的最早發生時間$VE[i_{1}]....VE[i_{k}]$,那么事件$V_j$的最早發生時間就是$max(VE[i_{1}]+length[r_{1}],...,VE[i_{k}]+length[r_{k}])$,取最大值就是所有能到達$V_j$的活動中最后一個完成的時間,因為只有它們都完成后,$V_j$才算“激活”。
也就是有這樣一個式子$$VE\left[ j\right] =\max \left( VE\left[ i_{p}\right] +length\left[ r_{p}\right] \right),\ p=1,2,...,k$$
如果要計算出$VE[j]$的正確值,就必須在訪問$V_j$之前$VE[i_{1}]....VE[i_{k}]$都已經得到,也就是在訪問某個結點的時候保證它的前驅結點都已經訪問完畢了,這就需要用到上一篇的拓撲排序了,此部分代碼如下:
const int N = 30000; vector<pair<int, int>>G[N + 5];//first是下一個結點、second是權值 stack<int>topoOrder; void topologicalSort() { queue<int >q; for (int i = 1; i <= n; i++) if (inDegree[i] == 0) q.push(i); while (!q.empty()) { int u = q.front(); q.pop(); topoOrder.push(u); for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first; if (--inDegree[v] == 0) { q.push(v); } //用VE[u]來更新u的后繼結點 VE[v] = max(VE[u] + G[u][i].second, VE[v]); } } }
求VL數組
同理,根據$VL[i]=VL[j]-length[r]$,假設已經算好了事件$V_{j1},...V_{jk}$的最晚發生時間$VL[j_{1}]....VL[j_{k}]$,那么事件$V_i$的最晚發生時間就是$min(VL[j_{1}]-length[r_{1}],...,VL[j_{k}]-length[r_{k}])$,取最小值就是取所有從$V_i$出發的活動的最早開始的時間,因為必須滿足所有$V_{j1},...V_{jk}$不會延期。
也就是有這樣一個式子$$VL\left[ i\right] =\min \left( VL\left[ j_{p}\right] -length\left[ r_{p}\right] \right),\ p=1,2,...,k$$
跟$VE$數組相類似,如果想要計算出$VL[i]$的正確值,就必須在訪問$V_i$之前$VL[j_{1}]....VL[j_{k}]$都已經得到,跟求$VE$數組剛好相反,也就是在訪問某個結點的時候保證它的后繼結點都已經訪問完畢了,這就需要用到逆拓撲序列來實現,所以我們上面實現的時候用$Stack$把拓撲序列存了起來。此部分代碼如下:
const int inf = 1 << 30; //因為終點一定是關鍵事件,所以終點的最晚發生時間是等於最早發生時間 VL[n] = VE[n]; fill(VL, VL + n, inf); //上面兩句分開寫便於理解,這兩句可以寫成一句fill(VL,VL+n+1,VE[n]); //如果題目默認n是匯點,那么VE[n]就是最長路徑,給VL數組賦初值一樣可以起到inf的作用 //如果題目沒明確說明n是匯點,則遍歷一遍求VE的最大值,去代替VE[n]即可 while (!topoOrder.empty()) { int u = topoOrder.top(); topoOrder.pop(); for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first; VL[u] = min(VL[u], VL[v] - G[u][i].second); } }
主體代碼
最后只需要根據$$E[r]+length[r]=VE[j]\\ VL[j]=L[r]+length[r]$$計算出$E_i$和$L_i$,判斷是否相等即可,完整代碼如下:
const int N = 10000; vector<pair<int, int>>G[N + 5];//后繼節點、權值 stack<int>topoOrder; int n, m, inDegree[N + 5], VE[N + 5], VL[N + 5]; //結點編號為1~n void topologicalSort() { queue<int >q; for (int i = 1; i <= n; i++) if (inDegree[i] == 0) { q.push(i); } while (!q.empty()) { int u = q.front(); q.pop(); topoOrder.push(u); for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first; inDegree[v]--; if (inDegree[v] == 0) { q.push(v); } //用VE[u]來更新u的后繼節點 VE[v] = max(VE[u] + G[u][i].second, VE[v]); } } fill(VL, VL + n + 1, VE[n]); while (!topoOrder.empty()) { int u = topoOrder.top(); topoOrder.pop(); for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first; VL[u] = min(VL[u], VL[v] - G[u][i].second); } } for (int u = 1; u <= n; u++) { for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first, d = G[u][i].second; if (VE[u] == VL[v] - d ) { //u-->v是一條關鍵路徑 } } } }
例題
求關鍵路徑長度
http://acm.hdu.edu.cn/showproblem.php?pid=4109
這題只要求關鍵路徑長度,不要求列舉出來,那就只要把VE數組求出來即可
#include <iostream> #include <fstream> #include <algorithm> #include <queue> #include <stack> #include <stdio.h> #include <vector> using namespace std; const int N = 1000; vector<pair<int, int>>G[N + 5];//后繼節點、權值 int n, m, inDegree[N + 5], VE[N + 5], VL[N + 5]; void 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(); for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first; inDegree[v]--; if (inDegree[v] == 0) { q.push(v); } //用VE[u]來更新u的后繼節點 VE[v] = max(VE[u] + G[u][i].second, VE[v]); } } } int main() { #ifdef LOCAL fstream cin("data.in"); #endif // LOCAL //while (cin >> n >> m) { while (scanf("%d%d", &n, &m) != EOF) { for (int i = 0; i < n; i++) { G[i].clear(); VE[i] = inDegree[i] = 0; } for (int i = 0; i < m; i++) { int c1, c2, c3; scanf("%d%d%d", &c1, &c2, &c3); //cin >> c1 >> c2 >> c3; G[c1].push_back(make_pair(c2, c3)); inDegree[c2]++; } topologicalSort(); //終點不確定,遍歷找最大值 int res = 0; for (int i = 0; i < n; i++) { res = max(res, VE[i]); } printf("%d\n", res + 1);//按題目意思最小時間為1,所以需要+1 } return 0; }
標准版關鍵路徑
https://acm.sdut.edu.cn/onlinejudge2/index.php/Home/Index/problemdetail/pid/2498.html
最后要求輸出字典序最小的關鍵路徑,輸出時稍作一點點處理就行了。(題目沒有說明,但是數據默認$n$為匯點,交了之后才發現自己寫的好像不太對還是過了。。。)
#include <iostream> #include <fstream> #include <algorithm> #include <queue> #include <stack> #include <stdio.h> #include <vector> using namespace std; const int N = 10000; vector<pair<int, int>>G[N + 5];//后繼節點、權值 stack<int>topoOrder; int n, m, inDegree[N + 5], VE[N + 5], VL[N + 5]; void topologicalSort() { queue<int >q; for (int i = 1; i <= n; i++) if (inDegree[i] == 0) { q.push(i); } while (!q.empty()) { int u = q.front(); q.pop(); topoOrder.push(u); for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first; inDegree[v]--; if (inDegree[v] == 0) { q.push(v); } //用VE[u]來更新u的后繼節點 VE[v] = max(VE[u] + G[u][i].second, VE[v]); } } //int res = 0; //for (int i = 1; i <= n; i++) { // res = max(res, VE[i]); //} //printf("%d\n", res); printf("%d\n", VE[n]);//如果題目沒說明n就是匯點,這兩行的VE[n]都必須改成上面的res fill(VL, VL + n + 1, VE[n]); while (!topoOrder.empty()) { int u = topoOrder.top(); topoOrder.pop(); for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first; VL[u] = min(VL[u], VL[v] - G[u][i].second); } } int flag = -1; for (int u = 1; u <= n; u++) { for (int i = 0; i < G[u].size(); i++) { int v = G[u][i].first, d = G[u][i].second; if (VE[u] == VL[v] - d && (flag == -1 || u == flag)) { flag = v; cout << u << ' ' << v << endl; } } } } int main() { #ifdef LOCAL fstream cin("data.in"); #endif // LOCAL //while (cin >> n >> m) { while (scanf("%d%d", &n, &m) != EOF) { for (int i = 1; i <= n; i++) { G[i].clear(); VL[i] = VE[i] = inDegree[i] = 0; } for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); //cin >> u >> v >> w; G[u].push_back({ v, w }); inDegree[v]++; } topologicalSort(); } return 0; } /*************************************************** User name: vsdj Result: Accepted Take time: 52ms Take Memory: 1088KB Submit time: 2019-10-27 16:36:34 ****************************************************/
