《大話數據結構》——學習筆記(圖)

圖的定義

圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通過表示爲:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合(有窮非空),E是圖G中邊的集合(可以爲空)

這裏寫圖片描述

圖是一種較線性表和樹更加複雜的數據結構,在圖形結構中,結點之間的關係可以是任意的,圖中任意兩個數據元素之間都可能相關

各種圖定義

無向邊: 若頂點vivj 之間的邊沒有方向,則稱這條邊爲無向邊(Edge),用無序偶對(vi ,vj )來表示,如果圖中任意兩個頂點之間的邊都是無向邊,則稱該圖爲無向圖(Undirected graphs)

有向邊: 若從頂點vivj 的邊有方向,則稱這條邊爲有向邊,也稱爲弧(Arc),用有序偶<vi ,vj >來表示,vi 稱爲弧尾(Tail),vj 稱爲弧頭(Head),如果圖中任意兩個頂點之間的邊都是有向邊,則稱該圖爲有向圖(Directed graphs)

在圖中,若不存在頂點到其自身的邊,且同一條邊不重複出現,則稱這樣的圖爲簡單圖

在無向圖中,如果任意兩個頂點之間都存在邊,則稱該圖爲無向完全圖,含有n個頂點的無向完全圖有n×(n1)2 條邊

在有向圖中,如果任意兩個頂點之間都存在方向互爲相反的兩條弧,則稱該圖爲有向完全圖,含有n個頂點的有向完全圖有nx(n-1)條邊

有些圖的邊或弧具有與它相關的數字,這種與圖的邊或弧相關的數叫做權(Weight),這種帶權的圖通常稱爲網(Network)

假設有兩個圖G=(V,{E})和G’=(V’,{E’}),如果V’∈V且E’∈E,則稱G’爲G的子圖(Subgraph)

圖的頂點與邊間關係

對於無向圖G=(V,{E}),如果邊(v,v’)∈E,則稱頂點v和v’互爲鄰接點(Adjacent),即v和v’相鄰接,邊(v,v’)依附(incident)於頂點v和v’,或者說(v,v’)與頂點v和v’相關聯。頂點v的度(Degree)是和v相關聯的邊的數目,記爲TD(v)

對於有向圖G=(V,{E}),如果弧<v,v’>∈E,則稱頂點v鄰接到頂點v’,頂點v’鄰接自頂點v。弧<v,v’>和頂點v,v’相關聯,以頂點爲頭的弧的數目稱爲v的入度(InDegree),記爲ID(v),以v爲尾的弧的數目稱爲v的出度(OutDegree),記爲OD(v);頂點v的度爲TD(v)=ID(v)+OD(v)

路徑的長度是路徑上的邊或弧的數目

第一個頂點到最後一個頂點相同的路徑稱爲迴路或環(Cycle),序列中頂點不重複出現的路徑稱爲簡單路徑,除了第一個頂點和最後一個頂點外,其餘頂點不重複出現的迴路,稱爲簡單迴路或簡單環,如下左圖爲簡單環,右圖不是簡單環

<img shuju_46>

連通圖

在無向圖G中,如果從頂點v到頂點v’有路徑,則稱v和v’是連通的,如果對於圖中任意兩個頂點vivj ∈E,vivj 都是連通的,則稱G是連通圖(Connected Graph)

無向圖中的極大連通子圖稱爲連通分量,它強調:

  • 要是子圖
  • 子圖要是連通的
  • 連通子圖含有極大頂點數
  • 具有極大頂點數的連通子圖包含依附於這些頂點的所有邊

在有向圖G中,如果對於每一對vivj ∈V、vivj ,從vivj 和從vjvi 都存在路徑,則稱G是強連通圖。有向圖中的極大強連通子圖稱做有向圖的強連通分量,如下圖所示,左圖並不是強連通圖,右圖是強連通圖,且是左圖的極大強連通子圖,即是左圖的強連通分量

<img shuju_047>

一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的n個頂點,但只有足以構成一棵樹的n-1條邊,如下圖

<img shuju_048>

如果一個有向圖恰有一個頂點的入度爲0(根結點),其餘頂點的入度均爲1,則是一棵有向樹。一個有向圖的生成森林由若干棵有向樹組成,含有圖中全部頂點,但只有足以構成若干棵不相交的有向樹的弧,如下圖

<img shuju_049>

圖的存儲結構

鄰接矩陣

將圖分成頂點和邊或弧兩個結構來存儲,頂點不分大小、主次,所以用一個一維數組來存儲,而邊或弧由於是頂點與頂點之間的關係,所以用二維數組(稱爲鄰接矩陣)來存儲

設圖G有n個頂點,則鄰接矩陣是一個nxn的方陣,定義爲:

shuju_050

<img shuju_051>

無向圖的邊數組是一個對稱矩陣

有了這個矩陣,可以很容易地知道圖中的信息

  • 很容易判斷任意兩頂點是否有邊無邊
  • 某個頂點的度就是這個頂點vi 在鄰接矩陣中第i行(或第i列)的元素之和,如v1 的度就是1+0+1+0=2
  • 求頂點vi 的所有鄰接點就是將矩陣中第i行元素掃描一遍,arc[i][j]爲1就是鄰接點

有向圖的邊數組不是一個對稱矩陣,有向圖樣例如下

<img shuju_052>

網圖是每條邊上帶有權的圖,設圖G是網圖,有n個頂點,則鄰接矩陣是一個nxn的方陣,定義爲:

<img shuju_053>

這裏Wij 表示(vi ,vj )或<vi ,vj >上的權值,∞表示一個計算機允許的、大於所有邊上權值的值,也就是一個不可能的極限值

<img shuju_054>

缺點: 對於邊數相對於頂點較少的圖,這種結構是對存儲空間的極大浪費

鄰接表

數組與鏈表相結合的存儲方法稱爲鄰接表(Adjacency List)

鄰接表的處理方法:

  • 圖中頂點用一個一維數組存儲,每個數據元素還需要存儲指向第一個鄰接點的指針,以便於查找該頂點的邊信息
  • 圖中每個頂點vi 的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所以用單鏈表存儲,無向圖稱爲頂點vi 的邊表,有向圖則稱爲頂點vi 作爲弧尾的出邊表

<img shuju_055>

<img shuju_056>

對於帶權值的網圖,可以在邊表結點定義中再增加一個weight的數據域,存儲權值信息

<img shuju_057>

缺點: 對於有向圖來說,鄰接表關心了出度問題,想了解入度就必須要遍歷整個圖才能知道,反之,逆鄰接表解決了入度卻不瞭解出度的情況

十字鏈表

把鄰接表與逆鄰接表結合起來就組成了十字鏈表(Orthogonal List)

頂點表結點結構如下表所示

data firstin firstout

其中firstin表示入邊表頭指針,指向該頂點的入邊表中第一個結點,firstout表示出邊表頭指針,指向該頂點的出邊表中的第一個結點

邊表結點結構如下表所示

tailvex headvex headlink taillink

其中tailvex是指弧起點在頂點表的下標,headvex是指弧終點在頂點表中的下標,headlink是指入邊表指針域,指向終點相同的下一條邊,taillink是指邊表指針域,指向起點相同的下一條邊,如果是網,還可以再增加一個weight域來存儲權值

<img shuju_058>

對於v0 來說,它有兩個頂點v1v2 的入邊,因此v0 的firstin指向頂點v1 的邊表結點中headvex爲0的結點,如圖中①。接着由入邊結點的headlink指向下一個入邊頂點v2 ,如圖中的②。對於頂點v1 ,它有一個入邊頂點v2 ,所以它的firstin指向頂點v2 的邊表結點中headvex爲1的結點,如圖中③。頂點v2v3 也是同樣有一個入邊頂點,如圖中④和⑤

十字鏈表的好處就是把鄰接表和逆鄰接表整合在了一起,這樣既容易找到一vi 爲尾的弧,也容易找到以vi 爲頭的弧,因而容易求得頂點的出度和入度

鄰接多重表

在無向圖的應用中,如果關注的重點是頂點,那麼鄰接表是不錯的選擇,但如果更關注邊的操作,比如對已訪問過的邊做標記,刪除某一條邊等操作,就意味着,需要找到這條邊的兩個邊表結點進行操作,這還是比較繁瑣的

對十字鏈表的邊表結點的結構進行一些改造,就可以避免這個問題

重新定義的邊表結點結構如下所示

ivex ilink jvex jlink

其中ivex和jvex是與某條邊依附的兩個頂點在頂點表中下標,ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊,這就是鄰接多重表結構

<img shuju_059>

首先連線的①②③④就是將頂點的firstedge指向一條邊,頂點下標要與ivex的值相同,接着,由於頂點v0 的(v0 ,v1 )邊的鄰邊有(v0 ,v3 )和(v0 ,v2 )。因此⑤⑥的連線就是滿足指向下一條依附於頂點v0 的邊的目標,注意ilink指向的結點的jvex一定要和它本身的ivex的值相同。同樣,連線⑦就是指(v1 ,v0 )這條邊,它是相當於頂點v1 指向(v1 ,v2 )邊後的下一條,v2 有三條邊依附,所以在③之後就有了⑧⑨。連線⑩的就是頂點v3 在連線④之後的下一條邊

鄰接多重表與鄰接表的差別,僅僅是在於同一條邊在鄰接表中用兩個節點表示,而在鄰接多重表中只有一個結點,這樣對邊的操作就方便多了,若刪除上圖的(v0 ,v2 )這條邊,只需要將⑥⑨的鏈接指向改爲^即可

邊集數組

邊集數組是由兩個一維數組構成,一個是存儲頂點的信息;另一個是存儲邊的信息,這個邊數組每個數據元素由一條邊的起點下標(begin)、終點下標(end)和權(weight)組成

<img shuju_061>

邊集數組關注的是邊的集合,在邊集數組中要查找一個頂點的度需要掃描整個邊數組,效率並不高,因此它更適合對邊依次進行處理操作,而不適合對頂點相關的操作

圖的遍歷

從圖中某一頂點出發訪遍圖中其餘頂點,且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)

深度優先遍歷

深度優先遍歷(Depth_First_Search),也稱爲深度優先搜索,簡稱DFS,類似於樹的前序遍歷

<img shuju_062>

從頂點A開始,在沒有碰到重複頂點的情況下,始終是向右手邊走,當走到H處發現沒有通道沒走過,此時一層層向上返回,把沒有走過的通道標記,如D->I,直到返回頂點A

對於n個頂點e條邊的圖來說,鄰接矩陣由於是二維數組,要查找每個頂點的鄰接點需要訪問矩陣中的所有元素,因此需要O(n2 )的時間。而鄰接表做存儲結構時,找鄰接點所需的時間取決於頂點和邊的數量,所以是O(n+e)。顯然對於點多邊少的稀疏圖來說,鄰接表結構使得算法在時間效率上大大提高

廣度優先遍歷

廣度優先遍歷(Breadth_First_Search),又稱廣度優先搜索,簡稱BFS,類似於樹的層序遍歷

先將下圖變形成層序結構,變形原則是頂點A放置在第一層,讓與它有邊的頂點B、F爲第二層,再讓與B和F有邊的頂點C、I、G、E爲第三層,再將這四個頂點有邊的D、H放在第四層

<img shuju_064>

圖的深度優先遍歷與廣度優先遍歷算法在時間複雜度上是一樣的,不同之處在於對頂點訪問的順序不同

深度優先更適合目標比較明確,以找到目標爲主要目的的情況,而廣度優先更適合在不斷擴大遍歷範圍時找到相對最優解的情況

最小生成樹

把構造連通網的最小代價生成樹稱爲最小生成樹(Minimum Cost Spanning Tree)

找連通網的最小生成樹,經典的有兩種算法,普里姆算法和克魯斯卡爾算法

普里姆(Prim)算法

定義: 假設N=(P,{E})是連通網,TE是N上最小生成樹中邊的集合。算法從U={u0 }(u0 ∈V)
,TE={}開始。重複執行下述操作:在所有u∈U,v∈V-U的邊(u,v)∈E中找一條代價最小的邊(u0 ,v0 ),
併入集合TE,同時v0 併入U,直至U=V爲止。此時TE中必有n-1條邊,則T=(V,{TE})爲N的最小生成樹

普里姆(Prim)算法是以某頂點爲起點,逐步找各頂點上最小權值的邊來構建最小生成樹的

此算法的時間複雜度爲O(n2 )

示例:

<img shuju_065>

上圖中左圖G有9個頂點,它的arc二維數組如上圖右圖所示,數組中用65535來代表∞

用普里姆算法解析過程如下:

/* Prim算法生成最小生成樹 */

void MiniSpanTree_Prim(MGraph G){
    int min, i ,j, k;
    int adjvex[MAXVEX]; /* 保存相關頂點下標 */
    int lowcost[MAXVEX]; /* 保存相關頂點間邊的權值 */
    lowcost[0] = 0; /* 初始化第一個權值0,即v0加入生成樹 */
    adjvex[0] = 0; /* 初始化第一個頂點下標爲0 */
    for(i = 1; i < G.numVertexes; i++){
        lowcost[i] = G.arc[0][i]; /* 將v0頂點與之有邊的權值存入數組 */
        adjvex[i] = 0; /* 初始化都爲v0的下標 */
    }
    for(i = 1; i < G.numVertexes; i++){
        min = INFINITY; /* 初始化最小權值爲∞ */
        j = 1; k = 0;
        while(j < G.numVertexes){ /* 循環全部頂點 */
            if(lowcost[j] != 0 && lowcost[j] < min){
                /* 如果權值不爲0且權值小於min */
                min = lowcot[j]; /* 則讓當前權值成爲最小值 */
                k = j; /* 將當前最小值的下標存入k */
            }
            j++;
        }
        printf("(%d, %d)", adjvex[k], k); /* 打印當前頂點邊中權值最小邊 */
        lowcost[k] = 0; /* 將當前頂點的權值設置爲0,表示此頂點已經完成任務 */
        for(j = 1; j < G.numVertexes; j++){
            /* 循環所有頂點 */
            if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j]){
                /* 若下標爲k頂點各邊權值小於此前這些頂點未被加入生成樹權值 */

                lowcost[j] = G.arc[k][j]; /* 將較小權值存入lowcost */
                adjvex[j] = k; /* 將下標爲k的頂點存入adjvex */
            }
        }
    }

}

最終構造過程如下圖所示

<img shuju_066>

克魯斯卡爾(Kruskal)算法

定義: 假設N={V,{E}}是連通網,則令最小生成樹的初始狀態爲只有n個頂點而無邊的非連通圖T={V,{}},圖中每個頂點自成一個連通分量,在E中選擇代價最小的邊,若該邊依附的頂點落在T中不同的連通分量上,則將此邊加入到T中,否則捨去此邊而選擇下一條代價最小的邊。依次類推,直至T中所有頂點都在同一連通分量上爲止

克魯斯卡爾(Kruskal)算法是以邊爲目標去構建,因爲權值是在邊上,直接去找最小權值的邊來構建生成樹也是很自然的想法,只不過構建時要考慮是否會形成環路而已

示例:

<img shuju_067>

上圖將左圖轉化成右圖的邊集數組,並且對它們按權值從小到大排序

克魯斯卡爾算法代碼如下:

/* Kruskal算法生成最小生成樹 */
void MiniSpanTree_Kruskal(MGraph G) {
    int i, n, m;
    Edge edges[MAXEDGE]; /* 定義邊集數組 */
    int parent[MAXVEX]; /* 定義一數組用來判斷邊與邊是否形成環路 */
    /* 此處省略將鄰接矩陣G轉化爲邊集數組edges並按權由小到大排序的代碼 */
    for(i = 0; i < G.numVertexes; i++){
        parent[i] = 0; /* 初始化數組值爲0 */
    }
    for(i = 0; i < G.numEdges; i++){
        /* 循環每一條邊 */
        n = Find(parent, edges[i].begin);
        m = Find(parent, edges[i].end);
        if(n != m){
            /* 假如n與m不等,說明此邊沒有與現有生成樹形成環路 */
            parent[n] = m; /* 將此邊的結尾頂點放入下標爲起點的parent中,表示此頂點已經在生成樹集合中 */
            printf("(%d, %d) %d", edges[i].begin, edges[i].end, edges[i].weight);
        }
    }
}

int Find(int *parent, int f){
    /* 查找連線頂點的尾部下標 */
    while(parent[f] > 0){
        f = parent[f];
    }
    return f;
}

此算法的Find函數由邊數e決定,時間複雜度爲O(e ),而外面有一個for循環e次,所以克魯斯卡爾算法的時間複雜度爲O(ee )

對比兩個算法,克魯斯卡爾算法主要是針對邊展開,邊數少時效率會非常高,所以對於稀疏圖有很大的優勢;而普里姆算法對於稠密圖,即邊數非常多的情況會更好一些

最短路徑

在網圖和非網圖中,最短路徑的含義是不同的,由於非網圖它沒有邊上的權值,所謂的最短路徑,其實就是指兩頂點之間經過的邊數最少的路徑;而對於網圖來說,最短路徑,是指兩頂點之間經過的邊上權值之和最少的路徑,並且稱路徑上的第一個頂點是源點,最後一個頂點是終點

迪傑斯特拉(Dijkstra)算法

這是一個按路徑長度遞增的次序產生最短路徑的算法,它的思路大體是這樣的,從源點開始,一步步求出從源點到終點間所有頂點的最短路徑,過程都是基於已經求出的最短路徑的基礎上,求出更遠頂點的最短路徑

迪傑斯特拉的算法如下:

#define MAXVEX 9 
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX]; /* 用於存儲最短路徑下標的數組 */
typedef int ShortPathTable[MAXVEX]; /* 用於存儲到各點最短路徑的權值和 */
/* Dijkstra算法,求有向網G的v0頂點到其餘頂點v最短路徑P[v]及帶權長度D[v],P[v]的值爲前驅頂點下標,D[v]表示v0到v的最短路徑長度和 */
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx * P, ShortPathTable *D){
    int v, w, k, min;
    int final[MAXVEX]; /* final[w]=1表示求得頂點v0到vw的最短路徑 */
    for(v = 0; v < G.numVertexes; v++){
        /* 初始化數據 */
        final[v] = 0; /* 全部頂點初始化爲未知最短路徑狀態 */
        (*D)[v] = G.matirx[v0][v]; /* 將與v0點有連線的頂點加上權值 */
        (*P)[v] = 0; /* 初始化路徑數組P爲0 */
    }
    (*D)[v0] = 0; /* v0至v0路徑爲0 */
    final[v0] = 1; /* v0至v0不需要求路徑 */
    /* 開始主循環,每次求得v0到某個v頂點的最短路徑 */
    for(v = 1; v < G.numVertexes; v++){
        min = INFINITY; /* 當前所知離v0頂點的最近距離 */
        for(w = 0; w < G.numVertexes; w++){
            /* 尋找離v0最近的頂點 */
            if(!final[w] && (*D)[w] < min){
                k = w;
                min = (*D)[w]; /* w頂點離v0頂點更近 */
            }
        }
        final[k] = 1; /* 將目前找到的最近的頂點置爲1 */
        for(w = 0; w < G.numVertexes; w++){
            /* 修正當前最短路徑及距離 */
            /* 如果經過v頂點的路徑比現在這條路徑的長度短的話 */
            if(!final[w] && (min + G.matirx[k][w] < (*D)[w])){
                /* 說明找到了更短的路徑,修改D[w]和P[w] */
                (*D)[w] = min + G.matirx[k][w]; /* 修改當前路徑長度 */
                (*P)[w] = k;
            }
        }
    }
}

通過迪傑斯特拉(Dijkstra)算法解決了從某個源點到其餘各頂點的最短路徑問題,從循環嵌套可以很容易得到此算法的時間複雜度爲O(n2 ),求所有頂點到所有頂點的時間複雜度爲O(n3 )

弗洛伊德(Floyd)算法

如下圖,要求出所有頂點到所有頂點的最短路徑

<img shuju_070>

先定義兩個二維數組D1 [3][3]和P1 [3][3],D1 代表頂點到頂點的最短路徑權值和的矩陣。P1 代表對應頂點的最小路徑的前驅矩陣。通過分析,v1 ->v0 ->v2 得到D1 [1][0]+D1 [0][2]=3 要小於 v1 ->v2 得到的D1 [1][2]=5,所以修正D1 [1][2]=D1 [1][0]+D1 [0][2],P1 [1][2]也修改爲當前中轉的頂點v0 的下標0,也就是說

D0 [v][w]=min{D1 [v][w], D1 [v][0] + D1 [0][w]}

示例:

<img shuju_069>

弗洛伊德算法如下:

typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
/* Floyd算法,求網圖G中各頂點v到其餘頂點w最短路徑P[v][w]及帶權長度D[v][w] */
void ShortestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D){
    int v, w, k;
    for(v = 0; v < G.numVertexes; ++v){
        /* 初始化D與P */
        for(w = 0; w < G.numVertexes; ++w){
            (*D)[v][w] = G.matirx[v][w]; /* D[v][w]值即爲對應點間的權值 */
            (*P)[v][w] = w; /* 初始化P */
        }
    }
}

for(k = 0; k < G.numVertexes; ++k){
    for(v = 0; v < G.numVertexes; ++v){
        for(w = 0; w < G.numVertexes; ++w){
            if((*D)[v][w] > (*D)[v][k] + (*D)[k][w]){
                /* 如果經過下標爲k頂點路徑比原兩點間路徑更短,將當前兩點間權值設爲更小的一個 */
                (*D)[v][w] = (*D)[v][k] + (*D)[k][w];
                (*P)[v][w] = (*P)[v][k]; /* 路徑設置經過下標爲k的頂點 */
            }
        }
    }
}

如果需要求所有頂點至所有頂點的最短路徑問題,弗洛伊德(Floyd)算法是不錯的選擇

拓撲排序

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

設G={V,E}是一個具有n個頂點的有向圖,V中的頂點序列v1v2 … 滿足若從頂點vivj 有一條路徑,則在頂點序列中頂點vi 必須在頂點vj 之前,則稱這樣的頂點序列爲一個拓撲序列

拓撲排序就是對一個有向圖構造拓撲序列的過程,構造時會有兩個結果,如果此網的全部頂點都被輸出,則說明它是不存在環(迴路)的AOV網;如果輸出頂點數少了,說明這個網存在環(迴路),不是AOV網

一個不存在迴路的AOV網,可以應用在各種各樣的工程或項目的流程圖中,滿足各種應用場景的需要

拓撲排序算法

對AOV網進行拓撲排序的基本思路是:從AOV網中選擇一個入度爲0的頂點輸出,然後刪除此頂點,並刪除以此頂點爲尾的弧,繼續重複此步驟,直到輸出全部頂點或者AOV網中不存在入度爲0的頂點爲止

先爲AOV網建立一個鄰接表,如下圖所示

<img shuju_071>
<img shuju_072>

拓撲排序算法實現如下:

/* 拓撲排序,若GL無迴路,則輸出拓撲排序序列並返回OK,若有迴路返回ERROR */
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(GL->adjList[i].in == 0){
            stack[++top] = i; /* 將入度爲0的頂點入棧 */
        }
    }
    while(top != 0){
        gettop = stack[top--]; /* 出棧 */
        printf("%d -> ", GL->adjList[gettop].data); /* 打印此頂點 */
        count++; /* 統計輸出頂點數 */
        for(e = GL->adjList[gettop].firstedge; e; e = e->next){
            /* 對此頂點弧表遍歷 */
            k = e->adjList;
            if(!(--GL->adjList[k].in)){
                /* 將k號頂點鄰接點的入度減1 */
                stack[++top] = k; /* 若爲0則入棧,以便於下次循環輸出 */
            }
        }
    }
    if(count < GL->numVertexes){
        /* 如果count小於頂點數,說明存在環 */
        return ERROR;
    } else {
        return OK;
    }
}

整個過程就是先找到入度爲0的頂點,對該頂點對應的弧鏈表進行遍歷,將弧鏈表中的頂點的入度減一,不斷循環這個過程,最終輸出所有的頂點

<img shuju_073>

對一個具有n個頂點e條弧的AOV網來說,將入度爲0的頂點入棧的時間複雜度爲O(n),而之後的while循環中,每個頂點進一次棧,出一次棧,入度減1的操作共執行了e次,所以整個算法的時間複雜度爲O(n+e)

關鍵路徑

在一個表示工程的帶權有向圖中,用頂點表示事件,用有向邊表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,稱爲AOE網(Activity On Edge Network)

AOE網中沒有入邊的頂點稱爲始點或源點,沒有出邊的頂點稱爲終點或匯點,正常情況下,AOE網只有一個源點一個匯點

AOV網是頂點表示活動的網,它只描述活動之間的制約關係,而AOE網是用邊表示活動的網,邊上的權值表示活動持續的時間,因此AOE網是要建立在活動之間制約關係沒有矛盾的基礎之上,再來分析完成整個工程至少需要多少時間,或者爲縮短完成工程所需時間,應當加快哪些活動等問題

<img shuju_074>

把路徑上各個活動所持續的時間之和稱爲路徑長度,從源點到匯點具有最大長度的路徑叫做關鍵路徑,在關鍵路徑上的活動叫關鍵活動,如開始->發動機完成->部件集中到位->組裝完成就是關鍵路徑,路徑的長度爲5.5

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

關鍵路徑算法原理

只需要找到所有活動的最早開始時間和最晚開始時間,並且比較它們,如果相等就意味着此活動是關鍵活動,活動間的路徑爲關鍵路徑,如果不等,則就不是

爲此,需要定義如下幾個參數:

  • 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 的最晚發生時間,也就是不推遲工期的最晚開工時間

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

關鍵路徑算法

將AOE網轉化爲鄰接表結構,如下圖

這裏寫圖片描述

求事件的最早發生時間etv的過程,就是從頭至尾找拓撲序列的過程,因此,在求關鍵路徑之前,需要先調用一次拓撲序列算法的代碼來計算etv和拓撲序列列表

這裏寫圖片描述

由此可以得出計算頂點vk 即求etv[k]的最早發生時間的公式是:

這裏寫圖片描述

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

這裏寫圖片描述

求關鍵路徑的算法代碼如下:

/* 求關鍵路徑,GL爲有向圖,輸出GL的各項關鍵活動 */
void CriticalPath(GraphAdjList GL){
    EdgeNode *e;
    int i,gettop,k,j;
    int ele,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--]; /* 將拓撲序列出棧,後進先出 */
        for(e = GL->adjList[gettop].firstedge; e; e=e->next){
            /* 求各頂點事件的最遲發生時間ltv值 */
            k=e->adjvex;
            if(ltv[k]-e->weight < ltv[gettop]){
                /* 求各頂點事件最晚發生時間ltv */
                ltv[gettop] = ltv[k] - e->weight;
            }
        }
        for(j=0; j<GL->numVertexes; j++){
            /* 求ete,lte和關鍵活動 */
            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);
                }
            }
        }
    }
}

/* 拓撲排序,用於關鍵路徑計算 */
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; /* 初始化爲0 */
    etv=(int *)malloc(GL->numVertexes * sizeof(int)); /* 事件最早發生時間 */
    for(i=0; i<GL->numVertexes; i++){
        etv[i]=0; /* 初始化爲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;
    }
}

etv和ltv的數組求得如下圖

<img shuju_079>

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

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

<img shuju_080>

求關鍵路徑算法的時間複雜度爲O(n+e)

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