1. 程式人生 > >資料結構與演算法之------拓撲排序與關鍵路徑

資料結構與演算法之------拓撲排序與關鍵路徑

一.拓撲排序

這裡請結合參考部落格學習(在後面)

拓撲排序(無環圖的應用)

在一個表示工程的有向圖中,有頂點表示活動,用弧表示活動之間的優先關係,這樣的有向圖為頂點表示活動的網,我們稱為AOV(Activity On Vertex)網。

AOV網中的弧表示活動之間存在的某種制約關係。

所謂拓撲排序,其實就是對一個有向圖構造拓撲序列的過程。

執行步驟

由AOV網構造拓撲序列的拓撲排序演算法主要是迴圈執行以下兩步,直到不存在入度為0的頂點為止。

(1) 選擇一個入度為0的頂點並輸出之;

(2) 從網中刪除此頂點及所有出邊

迴圈結束後,若輸出的頂點數小於網中的頂點數,則輸出“有

迴路”資訊,否則輸出的頂點序列就是一種拓撲序列。 [2] 

 

非計算機應用

拓撲排序常用來確定一個依賴關係集中,事物發生的順序。例如,在日常工作中,可能會將專案拆分成A、B、C、D四個子部分來完成,但A依賴於B和D,C依賴於D。為了計算這個專案進行的順序,可對這個關係集進行拓撲排序,得出一個線性的序列,則排在前面的任務就是需要先完成的任務。

意:這裡得到的排序並不是唯一的!

就好像你早上穿衣服可以先穿上衣也可以先穿褲子,只要裡面的衣服在外面的衣服之前穿就行。

 演算法偽碼:

這種演算法的時間複雜度並不理想,有沒有更好的辦法呢?

當然是有的!!!

方法:隨時將入度變為0的頂點放到一個容器裡 

提醒一下:這裡的容器可以是很多,陣列,連結串列,堆疊都成

這裡我們用的是佇列

其中DAG(Directed Acyclic Graph)為有向無環圖 

一個小補充:

分別用佇列和堆疊作為容器,對計算機專業課程進行拓撲排序,得到的序列有什麼區別?用哪種容器排課更合理?

 

答案:

用堆疊的結果類似於DFS,課程是按照學科分支被一個分支一個分支地列出,相當於建議學生先把一個分支由淺入深學完,再進入下一個分支。

用佇列的結果類似於BFS,課程是按照深淺順序,一層一層地列出。這種排列更符合我們實際的教學計劃安排,即讓學生先在低年級學完基礎課程,再逐層深入。所以是用佇列更合理。

注意:無論用堆疊還是用佇列,都不影響拓撲序的正確性。堆疊或佇列的影響只是在面對一堆都不需要前序課的課程時,先輸出哪個的問題(結果可能會不同)。

演算法實現:

/* 鄰接表儲存 - 拓撲排序演算法 */
 
bool TopSort( LGraph Graph, Vertex TopOrder[] )
{ /* 對Graph進行拓撲排序,  TopOrder[]順序儲存排序後的頂點下標 */
    int Indegree[MaxVertexNum], cnt;
    Vertex V;
    PtrToAdjVNode W;
       Queue Q = CreateQueue( Graph->Nv );
  
    /* 初始化Indegree[] */
    for (V=0; V<Graph->Nv; V++)
        Indegree[V] = 0;
         
    /* 遍歷圖,得到Indegree[] */
    for (V=0; V<Graph->Nv; V++)
        for (W=Graph->G[V].FirstEdge; W; W=W->Next)
            Indegree[W->AdjV]++; /* 對有向邊<V, W->AdjV>累計終點的入度 */
             
    /* 將所有入度為0的頂點入列 */
    for (V=0; V<Graph->Nv; V++)
        if ( Indegree[V]==0 )
            AddQ(Q, V);
             
    /* 下面進入拓撲排序 */ 
    cnt = 0; 
    while( !IsEmpty(Q) ){
        V = DeleteQ(Q); /* 彈出一個入度為0的頂點 */
        TopOrder[cnt++] = V; /* 將之存為結果序列的下一個元素 */
        /* 對V的每個鄰接點W->AdjV */
        for ( W=Graph->G[V].FirstEdge; W; W=W->Next )
            if ( --Indegree[W->AdjV] == 0 )/* 若刪除V使得W->AdjV入度為0 */
                AddQ(Q, W->AdjV); /* 則該頂點入列 */ 
    } /* while結束*/
     
    if ( cnt != Graph->Nv )
        return false; /* 說明圖中有迴路, 返回不成功標誌 */ 
    else
        return true;
}

此外還有一個特別好的部落格(大話裡的知識講的也很清楚):https://www.cnblogs.com/Braveliu/p/3460232.html

看完我的建議必看這篇,會更好的理解拓撲排序演算法

二.關鍵路徑

這裡請結合參考部落格學習(在後面)

介紹

關鍵路徑通常(但並非總是)是決定專案工期的進度活動序列。它是專案中最長的路徑即使很小浮動也可能直接影響整個專案的最早完成時間。關鍵路徑的工期決定了整個專案的工期,任何關鍵路徑上的終端元素的延遲在浮動時間為零或負數時將直接影響專案的預期完成時間(例如在關鍵路徑上沒有浮動時間)。 [2]  但特殊情況下,如果總浮動時間大於零,則有可能不會影響專案整體進度。

一個專案可以有多個、並行的關鍵路徑。另一個總工期比關鍵路徑的總工期略少的一條並行路徑被稱為次關鍵路徑。最初,關鍵路徑方法只考慮終端元素之間的邏輯依賴關係。關鍵鏈方法中增加了資源約束。關鍵路徑方法是由杜邦公司發明的。

步驟

A、從開始頂點 v1 出發,令 ve(1)=0,按拓撲有序序列求其餘各頂點的可能最早發生時間。 [3] 

Ve(k)=max{ve(j)+dut(<j,k>)} , j ∈ T 。其中T是以頂點vk為尾的所有弧的頭頂點的集合(2 ≤ k ≤ n)。

如果得到的拓樸有序序列中頂點的個數小於網中頂點個數n,則說明網中有環,不能求出關鍵路徑,演算法結束。

B、從完成頂點

  

出發,令

  

,按逆拓撲有序求其餘各頂點的允許的最晚發生時間:

vl(j)=min{vl(k)-dut(<j,k>)} ,k ∈ S 。其中 S 是以頂點vj是頭的所有弧的尾頂點集合(1 ≤ j ≤ n-1)。

C、求每一項活動ai(1 ≤ i ≤ m)的最早開始時間e(i)=ve(j),最晚開始時間l(i)=vl(k)-dut(<j,k>) 。

若某條弧滿足 e(i)=l(i) ,則它是關鍵活動。

相關術語

(1)AOE網

用頂點表示事件,弧表示活動,弧上的權值表示活動持續的時間的有向圖叫AOE(Activity On Edge Network)網。在建築學中也稱為關鍵路線。AOE網常用於估算工程完成時間。例如:

圖1.有向網路

圖1 是一個網。其中有9個事件v1,v2,…,v9;11項活動a1,a2,…,a11。每個事件表示在它之前的活動已經完成,在它之後的活動可以開始。如 v1表示整個工程開始,v9 表示整個工程結束。V5表示活動a2和a3已經完成,活動a7和a8可以開始。與每個活動相聯絡的權表示完成該活動所需的時間。如活動a1需要6個時間單位可以完成。

一個AOE網的關鍵路徑可以不止一條。

只有在某頂點所代表的事件發生後,從該頂點出發的各有向邊所代表的活動才能開始。只有在進入某一頂點的各有向邊所代表的活動都已經結束,該頂點所代表的事件才能發生。表示實際工程計劃的AOE網應該是無環的,並且存在唯一的入度過為0的開始頂點和唯一的出度為0的完成頂點。

(2) 活動開始的最早時間e(i);

(3) 活動開始的最晚時間l(i) 定義e(i)=l(i)的活動叫關鍵活動;

(4) 事件開始的最早時間ve(i);

(5) 事件開始的最晚時間vl(i)。

應用

(1) 完成整個工程至少需要多少時間;

(2) 哪些活動是影響工程的關鍵。

 

兩個問題:

a.整個工期有多長? 

b.哪幾個組有機動時間

演算法

(1) 輸入e條弧<j,k>,建立AOE網的儲存結構;

(2) 從源點v1出發,令ve(1)=0,求 ve(j) ,2<=j<=n;

(3) 從匯點vn出發,令vl(n)=ve(n),求 vl(i), 1<=i<=n-1;

(4) 根據各頂點的ve和vl值,求每條弧s(活動)的最早開始時間e(s)和最晚開始時間l(s),其中e(s)=l(s)的為關鍵活動。

求關鍵路徑是在拓撲排序的前提下進行的,不能進行拓撲排序,自然也不能求關鍵路徑。

演算法分析

(1) 求關鍵路徑必須在拓撲排序的前提下進行,有環圖不能求關鍵路徑;

(2) 只有縮短關鍵活動的工期才有可能縮短工期;

(3) 若一個關鍵活動不在所有的關鍵路徑上,減少它並不能減少工期;

(4) 只有在不改變關鍵路徑的前提下,縮短關鍵活動才能縮短整個工期。

演算法實現:

過程略複雜,參考百度百科的程式碼

後面也有很好的參考部落格

#include <iostream>
#include <cstdio>
#include<string.h>
using namespace std;
int n,m,w[1001][1001],prev[1001],queue[1001],Time[1001],l=0,r=0,Pos[1001],path[1001];
void init()
{
    int i,a,b,c;
    scanf("%d%d",&n,&m);
    for (i=1; i<=m; i++)
    {
        scanf("%d%d%d",&a,&b,&c);
        w[a][b]=c;
        prev[b]++;
    }
}
inline void Newq(int v)
{
    r++;
    queue[r]=v;
}
inline void Del(int v)
{
    int i;
    for (i=1; i<=n; i++)
        if (w[v][i])
        {
            prev[i]--;
            if (!prev[i])
                Newq(i);
        }
}
void topo()
{
    for (int i=1; i<=n; i++)
        if (!prev[i])
            Newq(i);
    while (r<n)
    {
        l++;
        Del(queue[l]);
    }
}
void crucialpath()
{
    int i,j;
    memset(Time,0,sizeof(Time));
    for (i=1; i<=n; i++)
        for (j=1; j<=n; j++)
            if ((w[j][queue[i]]) && (Time[j]+w[j][queue[i]]>Time[queue[i]]))
            {
                Time[queue[i]]=Time[j]+w[j][queue[i]];
                Pos[queue[i]]=j;
            }
}
void print()
{
    printf("%d\n",Time[n]);
    int i=n,k=0;
    while (i!=1)
    {
        k++;
        path[k]=i;
    }
    for (i=k; i>1; i--)
        printf("%d ",path[i]);
    printf("%d\n",path[1]);
}
int main()
{
    init();
    topo();
    crucialpath();
    print();
    return 0;
}

 

此外還有一個特別好的部落格(大話裡的知識講的也很清楚):https://www.cnblogs.com/Braveliu/p/3461649.html