數據結構圖之六(關鍵路徑)

原文鏈接:http://www.cnblogs.com/Braveliu/p/3461649.html

【1】關鍵路徑

在我的經驗意識深處,“關鍵”二字一般都是指臨界點。

凡事萬物都遵循一個度的問題,那麼存在度就會自然有臨界點。

關鍵路徑也正是研究這個臨界點的問題。

在學習關鍵路徑前,先了解一個AOV網和AOE網的概念:

用頂點表示活動,用弧表示活動間的優先關係的有向圖:

稱爲頂點表示活動的網(Activity On Vertex Network),簡稱爲AOV網。

與AOV網對應的是AOE(Activity On Edge)網即邊表示活動的網。

AOE網是一個帶權的有向無環圖。

網中只有一個入度爲零的點(稱爲源點)和一個出度爲零的點(稱爲匯點)。

其中,頂點表示事件(Event),弧表示活動,權表示活動持續的時間。

通常,AOE網可用來估算工程的完成時間。

假如汽車生產工廠要製造一輛汽車,製造過程的大概事件和活動時間如上圖AOE網:

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

那麼,顯然對上圖AOE網而言,所謂關鍵路徑:

開始-->發動機完成-->部件集中到位-->組裝完成。路徑長度爲5.5。

如果我們試圖縮短整個工期,去改進輪子的生產效率,哪怕改動0.1也是無益的。

只有縮短關鍵路徑上的關鍵活動時間纔可以減少整個工期的長度。

例如如果製造發動機縮短爲2.5天,整車組裝縮短爲1.5天,那麼關鍵路徑爲4.5。

工期也就整整縮短了一天時間。

好吧! 那麼研究這個關鍵路徑意義何在?

假定上圖AOE網中弧的權值單位爲小時,而且我們已經知道黑深色的那一條爲關鍵路徑。

假定現在上午一點,對於外殼完成事件而言,爲了不影響工期:

外殼完成活動最早也就是一點開始動工,最晚在兩點必須要開始動工。

最大權值3表示所有活動必須在三小時之後完成,而外殼完成只需要2個小時。

所以,這個中間的空閒時間有一個小時,爲了不影響整個工期,它必須最遲兩點動工。

那麼纔可以保證3點時與發動機完成活動同時竣工,爲後續的活動做好準備。

對AOE網有待研究的問題是:

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

(2)那些活動是影響工程進度的關鍵?

今天研究是實例如下圖所示:

假想是一個有11項活動的AOE網,其中有9個事件(V1,V2,V3...V9)。

每個事件表示在它之前的活動已經完成,在它之後的活動可以開始。

如V1表示整個工程開始,V9表示整個共結束,V5表示a4和a5已經完成,a7和a8可以開始。

【2】關鍵路徑算法

爲了更好的理解算法,我們先需要定義如下幾個參數:

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

然後根據最早開工時間ete[k]和最晚開工時間lte[k]相等判斷ak是否是關鍵路徑。

將AOE網轉化爲鄰接表結構如下圖所示:


與拓撲序列鄰接表結構不同的地方在於,弧鏈表增加了weight域,用來存儲弧的權值。

求事件的最早發生時間etv的過程,就是從頭至尾找拓撲序列的過程。

因此,在求關鍵路徑之前,先要調用一次拓撲序列算法的代碼來計算etv和拓撲序列表。

數組etv存儲事件最早發生時間

數組ltv存儲事件最遲發生時間

全局棧用來保存拓撲序列

注意代碼中的粗部分與原拓撲序列的算法區別。

第11-15行 初始化全局變量etv數組。

第21行 就是講要輸出的拓撲序列壓入全局棧。

第 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]的最早發生時間公式如上。

下面具體分析關鍵路徑算法:

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

2.  第6行,調用求拓撲序列的函數。

  執行完畢後,全局數組etv和棧的值如下所示796,也就是說已經確定每個事件的最早發生時間。

3.  第7-9行初始化數組ltv,因爲etv[9]=27,所以數組當前每項均爲27。

4.  第10-19行爲計算ltv的循環。第12行,先將全局棧的棧頭出棧,由後進先出得到gettop=9。

  但是,根據鄰接表中信息,V9沒有弧。所以至此退出循環。

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,25,13。

  此時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)=15

  過程分析如下圖所示:

  當程序執行到第20行時,相關變量的值如下圖所示。

  比如etv[1]=3而ltv[1]=7表示(如果單位按天計的話):

  哪怕V1這個事件在第7天才開始也是可以保證整個工程按期完成。

  你也可以提前V1時間開始,但是最早也只能在第3天開始。

8.  第20-31行是求另兩個變量活動最早開始時間ete和活動最晚時間lte。

  當 j=0 時,從V0頂點開始,有<V0,V2>和<V0,V1>兩條弧。

  當 k=2 時,ete=etv[j]=etv[0]=0

  lte=ltv[k]-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[2]-len<v0,v1>=7-3=4  此時ete != lte

  表示弧<v0,v1>並不是關鍵活動。如圖所示:

說明:ete表示活動<Vk,Vj>的最早開工時間,是針對弧來說的。

但是隻有此弧的弧尾頂點Vk的事件發生了,它纔可以開始,ete=etv[k]。

lte表示的是活動<Vk,Vj>最晚開工時間,但此活動再晚也不能等V1事件發生纔開始。

而必須要在V1事件之前發生,所以lte=ltv[j]-len<Vk,Vj>。

9.  j=1 直到 j=9 爲止,做法完全相同。

最終關鍵路徑如下圖所示:

注意:本例是唯一一條關鍵路徑,並不等於不存在多條關鍵路徑。

如果是多條關鍵路徑,則單是提高一條關鍵路徑上的關鍵活動速度並不是能導致整個工程縮短工期、

而必須提高同時在幾條關鍵路徑上的活動的速度。

【3】關鍵路徑是代碼實現

本示例代碼與算法有些不同,但是效果相同,都是爲了達到一個共同目的:理解並學習關鍵路徑算法。

#include <iostream>
#include "Stack.h"
#include <malloc.h>
using namespace std;

#define  MAXVEX   10
#define  MAXEDGE  13

// 全局棧
SeqStack<int> sQ2;

typedef struct EdgeNode
{
    int adjvex;    // 鄰接點域,存儲該頂點對應的下標
    int weight; // 邊的權值
    struct EdgeNode* next; // 鏈域
} EdgeNode;

typedef struct VertexNode
{
    int inNum;    // 頂點入度值
    int data;    // 頂點數值欲
    EdgeNode* firstedge; // 邊表頭指針
} VertexNode, AdjList[MAXVEX];

typedef struct
{
    AdjList adjList;
    int numVertexes, numEdges; // 圖中當前頂點數和邊數(對於本案例,已經存在宏定義)
} graphAdjList, *GraphAdjList;

// 構建節點
EdgeNode* BuyNode()
{
    EdgeNode* p = (EdgeNode*)malloc(sizeof(EdgeNode));
    p->adjvex = -1;
    p->next = NULL;
    return p;
}
// 初始化圖
void InitGraph(graphAdjList& g)
{
    for (int i = 0; i < MAXVEX; ++i)
    {
        g.adjList[i].firstedge = NULL;
    }
}
// 創建圖
void CreateGraph(graphAdjList& g)
{
    int i = 0, begin = 0, end = 0, weight = 0;
    EdgeNode *pNode = NULL;
    cout << "輸入10個頂點信息(頂點 入度):" << endl;
    for (i = 0; i < MAXVEX; ++i)
    {
        cin >> g.adjList[i].data >> g.adjList[i].inNum;
    }
    cout << "輸入13條弧的信息(起點 終點 權值):" << endl;
    for (i = 0; i < MAXEDGE; ++i)
    {
        cin >> begin >> end >> weight;
        pNode = BuyNode();
        pNode->adjvex = end;
        pNode->weight = weight;
        pNode->next = g.adjList[begin].firstedge;
        g.adjList[begin].firstedge = pNode;
    }
}
// 打印輸入信息的邏輯圖
void PrintGraph(graphAdjList &g)
{
    cout << "打印AOE網的鄰接表邏輯圖:" << endl;
    for (int i = 0; i < MAXVEX; ++i)
    {
        cout << " " << g.adjList[i].inNum << " " << g.adjList[i].data << " ";
        EdgeNode* p = g.adjList[i].firstedge;
        cout << "-->";
        while (p != NULL)
        {
            int index = p->adjvex;
            cout << "[" << g.adjList[index].data << " " << p->weight << "] " ;
            p = p->next;
        }
        cout << endl;
    }
}
// 求拓撲序列
bool TopologicalSort(graphAdjList g, int* pEtv)
{
    EdgeNode* pNode = NULL;
    int i = 0, k = 0, gettop = 0;
    int nCnt = 0;
    SeqStack<int> sQ1;
    for (i = 0; i < MAXVEX; ++i)
    {
        if (0 == g.adjList[i].inNum)
            sQ1.Push(i);
    }
    for (i = 0; i < MAXVEX; ++i)
    {
        pEtv[i] = 0;
    }
    while (!sQ1.IsEmpty())
    {
        sQ1.Pop(gettop);
        ++nCnt;
        sQ2.Push(gettop); // 將彈出的頂點序號壓入拓撲序列的棧
        if (MAXVEX == nCnt)
        {    //去掉拓撲路徑後面的-->
            cout << g.adjList[gettop].data << endl; 
            break;
        }
        cout << g.adjList[gettop].data << "-->";
        pNode = g.adjList[gettop].firstedge;
        while (pNode != NULL)
        {
            k = pNode->adjvex;
            --g.adjList[k].inNum;
            if (0 == g.adjList[k].inNum)
                sQ1.Push(k);
            if (pEtv[gettop] + pNode->weight > pEtv[k])
                pEtv[k] = pEtv[gettop] + pNode->weight;
            pNode = pNode->next;
        }
    }
    return nCnt != MAXVEX;
}
// 關鍵路徑
void CriticalPath(graphAdjList g, int* pEtv, int* pLtv)
{
    // pEtv  事件最早發生時間
    // PLtv  事件最遲發生時間
    EdgeNode* pNode = NULL;
    int i = 0, gettop = 0, k =0, j = 0;
    int ete = 0, lte = 0; // 聲明活動最早發生時間和最遲發生時間變量
    for (i = 0; i < MAXVEX; ++i)
    {
        pLtv[i] = pEtv[MAXVEX-1]; // 初始化
    }
    while (!sQ2.IsEmpty())
    {
        sQ2.Pop(gettop); // 將拓撲序列出棧,後進先出
        pNode = g.adjList[gettop].firstedge;
        while (pNode != NULL)
        {    // 求各頂點事件的最遲發生時間pLtv值
            k = pNode->adjvex;
            if (pLtv[k] - pNode->weight < pLtv[gettop])
                pLtv[gettop] = pLtv[k] - pNode->weight;
            pNode = pNode->next;
        }
    }
    // 求 ete, lte, 和 關鍵路徑
    for (j = 0; j < MAXVEX; ++j)
    {
        pNode = g.adjList[j].firstedge;
        while (pNode != NULL)
        {
            k = pNode->adjvex;
            ete = pEtv[j]; // 活動最早發生時間
            lte = pLtv[k] - pNode->weight; // 活動最遲發生時間
            if (ete == lte)
                cout << "<V" << g.adjList[j].data << ",V" << g.adjList[k].data << "> :" << pNode->weight << endl;
            pNode = pNode->next;
        }
    }
}
void main()
{
    graphAdjList myg;
    InitGraph(myg);
    cout << "創建圖:" << endl;
    CreateGraph(myg);
    cout << "打印圖的鄰接表邏輯結構:" << endl;
    PrintGraph(myg);

    int* pEtv = new int[MAXVEX];
    int* pLtv = new int[MAXVEX];

    cout << "求拓撲序列(全局棧sQ2的值):" << endl;
    TopologicalSort(myg, pEtv);
    cout << "打印數組pEtv(各個事件的最早發生時間):" << endl;
    for(int i = 0; i < MAXVEX; ++i)
    {
        cout << pEtv[i] << " ";
    }
    cout << endl << "關鍵路徑:" << endl;

    CriticalPath(myg, pEtv, pLtv);
    cout << endl;
}
/*
創建圖:
輸入10個頂點信息(頂點 入度):
0 0
1 1
2 1
3 2
4 2
5 1
6 1
7 2
8 1
9 2
輸入13條弧的信息(起點 終點 權值):
0 1 3
0 2 4
1 3 5
1 4 6
2 3 8
2 5 7
3 4 3
4 6 9
4 7 4
5 7 6
6 9 2
7 8 5
8 9 3
打印圖的鄰接表邏輯結構:
打印AOE網的鄰接表邏輯圖:
0 0 -->[2 4] [1 3]
1 1 -->[4 6] [3 5]
1 2 -->[5 7] [3 8]
2 3 -->[4 3]
2 4 -->[7 4] [6 9]
1 5 -->[7 6]
1 6 -->[9 2]
2 7 -->[8 5]
1 8 -->[9 3]
2 9 -->
求拓撲序列(全局棧sQ2的值):
0-->1-->2-->3-->4-->6-->5-->7-->8-->9
打印數組pEtv(各個事件的最早發生時間):
0 3 4 12 15 11 24 19 24 27
關鍵路徑:
<V0,V2> :4
<V2,V3> :8
<V3,V4> :3
<V4,V7> :4
<V7,V8> :5
<V8,V9> :3
 */


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章