1. 程式人生 > >資料結構與演算法22-圖的關鍵路徑

資料結構與演算法22-圖的關鍵路徑

關鍵路徑

拓撲排序主要是為解決一個工程能否順序進行的問題,但有時我們還需要解決工程完成需要的最短時間問題。比如造一輛汽車,我們需要先造各種各樣零件、部件,最終再組裝成車,假如,造一個輪子需要0.5天時間,造一個發動機需要3天時間,造一個車底盤需要2天時間,造一個外殼需要2天時間,其他零部件時間需要2天,全部零部件集中到一處需要0.5天,組裝成車需要2天,問汽車廠造輛車,最短需要多少時間呢?

一定不是時間的全部和。因此我們如果要對一個流程圖獲得最短時間,就必須要分析它們的拓撲關係,並且找到當中最關鍵的流程,這個流程的時間就是最短時間。

因此在AOV網的基礎上,我們來提出一個概念:

在一個表示工程的帶權有向圖中,用頂點表示事件,用有向邊表示活動,用邊上的權表示活動的持續時間,這種有向圖的邊表示活動的網,我們稱之為AOE網(Activity  On Edge  Network)。我們把AOE網中沒有入邊的的頂點稱為始點或源點,沒有出邊的頂點稱為終點或匯點。由於一個工程,總有一個開始,一個結束,所以正常情況下,AOE網只有一個源點一個匯點。如下圖

 

v9是匯點,v0是源點。v1~v9分別表示事件,弧<v0,v1>,<v0,v2>,…<v8,v9>都表示一個活動,用a0,a1,…a12表示,它們的值代表著活動持續的時間,比如弧<v0,v1>就是從源點開始的第一個活動a0,它的時間是3個單位。

既然AOE網表示工程流程,所以它就具有明顯的工程特性。如有在某頂點所代表的事發生生,從該頂點出發的各活動才能開始。只有在進入某頂點的各活動都結束,該頂點所代表的事件才能發生。

儘管AOE與AOV網都是用來對工程建模的,但它們還是有很大的不同,主要體現在AOV網是頂點表示活動的網,它只描述活動之間的制約關係,而AOE網是用邊表示活動的網,邊上的權值表示活動持續的時間,因此,AOE網是要建立在活動之間制約關係沒有矛盾的基礎上,再來分析完成整個工程至少需要多少時間,或者為縮短完成工程所需要時間,應當加快哪些活動等問題。

 

 

我們把路徑上各個活動所持續的時間之和稱為路徑長度,從源點到匯點具有最大長度的路徑叫關鍵路徑,在關鍵路徑上的活動叫關鍵活動

最早開始時間與最晚開始時間不等,說明有空閒時間。也就是說,我們只需要找到所有活動的最早開始時間和最晚開始時間,並且比較它們,如果相等就意味著此活動是關鍵活動,活動間的路徑為關鍵路徑。如果不等就不是。為此有如下幾個引數

1.    事件的最早發生時間evt(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的最晚發生時間,也就是不推遲工期的最晚開工時間。

我們是由1和2可以求得3和4,然後再根據ete[k]是否與lte[k]相等來判斷ak是否是關鍵活動。

關鍵路徑演算法

我們將上面的AOE網轉化為鄰接表結構,注意增加了weight域,用來儲存弧的權值

 

求事件 的最早發生時間etv的過程,就是我們從頭至尾找拓撲序列的過程,因此,在求關鍵路徑之前,需要先呼叫一次拓撲序列演算法來計算etv和拓撲序列列表。為此我們首先在程式開始處宣告幾個全域性變數。

int  *etv,*ltv;//事件最早發生時間和最遲發生時間陣列

int  *stack2;//用於儲存拓撲序列的棧

int   top2; //用於stack2的指標

//下面是改進過的求拓撲序列演算法

status  TopologicalSort(GraphAdjList   GL)

{

      EdgeNode   *e;

      int   i,k,gettop;

      int   top=0;    //用於棧指標下標

      int    count=0;   //用於統計輸出頂點的個數

      int     *stack;  //創棧將入度為0的頂點入棧

     stack=(int  *)malloc(GL->numVertexes*sizeof(int));

     for(i=0;i<GL->numVertexes;i++)

          if(0==GL->adjList[i].in) stack[++top]=i;

      top2=0;

     etv=(int  *)malloc(GL-numVertexes*sizeof(int)); //事件最早發生時間

      for(i=0;i<GL->numVertexes;i++)

        etv[i] = 0;

     stack2=(int  *)malloc(GL->numVertexes*sizeof(int));

    while(top!=0)

    {

        gettop = stack[top--];

       count++;

       stack2[++top2] = gettop; //將彈出的頂點序號壓入拓撲序列的棧

        for(e=GL->adjList[gettop].firstedge;e;e=e->next)

        {

                k=e->adjvex;

               if(!(--GL->adjList[k].in))

                       stack[++top]=k;

             if((etv[gettop]+e->weight)>etv[k])

                        etv[k]=etv[gettop]+e->weight;

         }

    }

        if(count<GL->numVertexes)

            return ERROR;

        else

            return OK;

}

 

程式碼中,除了加粗部分外,與前面講的拓撲排序演算法沒有什麼不同,第11~15行為初始化全域性變數etv陣列、top2和stack2的過程。第21行就是將本是要輸出的拓撲序列壓入全域性棧stack2中。第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]的最早發生時間公式是:

 

 

下面我們來看一下求關鍵路徑的演算法程式碼

void  CriticalPath(GraphAdjList   GL)

{

        EdgeNode   *e;

        int   i,gettop,k,j;

        int ete,lte;

       TopologicalSort(GL);   //求拓撲序列,計算陣列etv和stack2的值

       ltv=(int  *)malloc(GL->numVertexes*sizeof(int));//事件最晚發生時間

       for(i=0;i<GL->numVertexes;i++)

                   ltv[i] =etv[GL->numVertexes-1];//初始化ltv

       while(top2!=0)

       {

                 gettop = stack2[top2-1];

                 for(e=GL->adjList[gettop].firstedge;e;e=e->next)

                 {//求各頂點事件的最遲發生時間ltv值

                       k=e->adjvex;

                      if(ltv[k]-e->weight<ltv[gettop])//各點事件最晚發生時間

                           ltv[gettop] =ltv[k]-e->weight;

                 }

                  for(j=0;j<GL->numVertexes;j++)

                 {

                         for(e=GL->adjList[j].firstedge;e;e=e->next)

                         {

                                k=e->adjvex;

                                ete = etv[j];

                                lte =ltv[k]-e->weight;

                                if(ete==lte)

                                   printf(“<v%d,v%d>length:%d,”,GL->adjList[j].data,GL->adjList[k].data,e->weight);

                          }

                  }

        }

}

 

1.    程式開始執行。第5行,聲明瞭ete和lte兩個活動最早最晚發生時間變數。

2.    第6行,呼叫求拓撲序列的函式。執行完畢後,全域性變數陣列etv和棧stack值。top2=10。也就是說,對於每個事件的最早發生時間,我們已經計算出來了

3.    第7~9行為初始化全域性變數ltv陣列,因為etv[9]=27,所以陣列ltv當前的值為:{27, 27, 27,27, 27, 27, 27, 27, 27}

4.    第10~19行為計算ltv的迴圈。第12行,先將stack2的棧頭出棧,由後進先出得到gettop = 0。根據鄰接表中,v9沒有弧表,所以第13~18行迴圈體未執行。

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、13、25,此時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)

 

此時你應該發現,我們在計算ltv時,其實是把拓撲序倒過來進行的。因此我們可以得出計算頂點vk即求ltv[k]的最晚發生時間的公式是:

 

 

就這樣,當程式執行到第20行時,相關變數的值如圖

 

相關變數的值上表,如果單位是天的話,比如etv=[1]而ltv[7],表示的意思就是如果時間單位是天的話,哪怕v1這個事件在第7天才開始,也可以保證整個工程的按期完成,你可以提前v1事件開始時間,但你最早也只能在第3三天開始。

8.    第20~31行是來求另兩個變數活動最早開始時間ete和活動最晚開始時間lte,並對相同下標的它們做比較。兩重迴圈巢狀是對鄰接表的頂點和每個頂點的弧表遍歷

 

9.    當j=0時,從v0點開始,有<v0,v2>和<v0,v1>兩條弧。當k=2時,ete=etv[j]=etv[0]。lte=ltv[v]-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[1]-len<v0,v1>=7-3=4,此時ete≠lte。因此<v0,v1>並不是關鍵活動

 

這裡需要解釋一下,ete本來是表示活動<vk,vj>的最早開工時間,是針對弧來說的。但是隻有此弧尾頂點vk的事件發生了,它才可以開始,因此ete=etv[k]

而lte表示的是活動<vk,vj>的最晚開工時間,但此活動再晚也不能等於vj事件發生才開始,而必須要在vj事件之前發生,所以lte=ltv[j]-len<vk,vj>。

比如晚上23點睡覺,你不能說到23點才開始做作業,而必須要提前2小時,在21點開始,才有可能按時完成作業。

所以最終,其實就是判斷ete與lte是否相等,相等意味著活動沒有任何空閒,是關鍵活動,否則就不是。

10.  j=1一直到j=9為止,做法完全相同的,關鍵路徑列印結果<v0,v2>4,<v2,v3>8<v3,v4>3,<v4,v7>4,<v7,v8>5,<v8,v9>3,最終關鍵路徑為如下圖