圖論篇5——關鍵路徑


引入

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;
}
View Code

標准版關鍵路徑

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
****************************************************/
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM