圖
圖的定義
圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通過表示爲:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合(有窮非空),E是圖G中邊的集合(可以爲空)
圖是一種較線性表和樹更加複雜的數據結構,在圖形結構中,結點之間的關係可以是任意的,圖中任意兩個數據元素之間都可能相關
各種圖定義
無向邊: 若頂點
有向邊: 若從頂點
在圖中,若不存在頂點到其自身的邊,且同一條邊不重複出現,則稱這樣的圖爲簡單圖
在無向圖中,如果任意兩個頂點之間都存在邊,則稱該圖爲無向完全圖,含有n個頂點的無向完全圖有
在有向圖中,如果任意兩個頂點之間都存在方向互爲相反的兩條弧,則稱該圖爲有向完全圖,含有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),序列中頂點不重複出現的路徑稱爲簡單路徑,除了第一個頂點和最後一個頂點外,其餘頂點不重複出現的迴路,稱爲簡單迴路或簡單環,如下左圖爲簡單環,右圖不是簡單環
連通圖
在無向圖G中,如果從頂點v到頂點v’有路徑,則稱v和v’是連通的,如果對於圖中任意兩個頂點
無向圖中的極大連通子圖稱爲連通分量,它強調:
- 要是子圖
- 子圖要是連通的
- 連通子圖含有極大頂點數
- 具有極大頂點數的連通子圖包含依附於這些頂點的所有邊
在有向圖G中,如果對於每一對
一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的n個頂點,但只有足以構成一棵樹的n-1條邊,如下圖
如果一個有向圖恰有一個頂點的入度爲0(根結點),其餘頂點的入度均爲1,則是一棵有向樹。一個有向圖的生成森林由若干棵有向樹組成,含有圖中全部頂點,但只有足以構成若干棵不相交的有向樹的弧,如下圖
圖的存儲結構
鄰接矩陣
將圖分成頂點和邊或弧兩個結構來存儲,頂點不分大小、主次,所以用一個一維數組來存儲,而邊或弧由於是頂點與頂點之間的關係,所以用二維數組(稱爲鄰接矩陣)來存儲
設圖G有n個頂點,則鄰接矩陣是一個nxn的方陣,定義爲:
無向圖的邊數組是一個對稱矩陣
有了這個矩陣,可以很容易地知道圖中的信息
- 很容易判斷任意兩頂點是否有邊無邊
- 某個頂點的度就是這個頂點
vi 在鄰接矩陣中第i行(或第i列)的元素之和,如v1 的度就是1+0+1+0=2 - 求頂點
vi 的所有鄰接點就是將矩陣中第i行元素掃描一遍,arc[i][j]爲1就是鄰接點
有向圖的邊數組不是一個對稱矩陣,有向圖樣例如下
網圖是每條邊上帶有權的圖,設圖G是網圖,有n個頂點,則鄰接矩陣是一個nxn的方陣,定義爲:
這裏
缺點: 對於邊數相對於頂點較少的圖,這種結構是對存儲空間的極大浪費
鄰接表
數組與鏈表相結合的存儲方法稱爲鄰接表(Adjacency List)
鄰接表的處理方法:
- 圖中頂點用一個一維數組存儲,每個數據元素還需要存儲指向第一個鄰接點的指針,以便於查找該頂點的邊信息
- 圖中每個頂點
vi 的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所以用單鏈表存儲,無向圖稱爲頂點vi 的邊表,有向圖則稱爲頂點vi 作爲弧尾的出邊表
對於帶權值的網圖,可以在邊表結點定義中再增加一個weight的數據域,存儲權值信息
缺點: 對於有向圖來說,鄰接表關心了出度問題,想了解入度就必須要遍歷整個圖才能知道,反之,逆鄰接表解決了入度卻不瞭解出度的情況
十字鏈表
把鄰接表與逆鄰接表結合起來就組成了十字鏈表(Orthogonal List)
頂點表結點結構如下表所示
data | firstin | firstout |
---|
其中firstin表示入邊表頭指針,指向該頂點的入邊表中第一個結點,firstout表示出邊表頭指針,指向該頂點的出邊表中的第一個結點
邊表結點結構如下表所示
tailvex | headvex | headlink | taillink |
---|
其中tailvex是指弧起點在頂點表的下標,headvex是指弧終點在頂點表中的下標,headlink是指入邊表指針域,指向終點相同的下一條邊,taillink是指邊表指針域,指向起點相同的下一條邊,如果是網,還可以再增加一個weight域來存儲權值
對於
十字鏈表的好處就是把鄰接表和逆鄰接表整合在了一起,這樣既容易找到一
鄰接多重表
在無向圖的應用中,如果關注的重點是頂點,那麼鄰接表是不錯的選擇,但如果更關注邊的操作,比如對已訪問過的邊做標記,刪除某一條邊等操作,就意味着,需要找到這條邊的兩個邊表結點進行操作,這還是比較繁瑣的
對十字鏈表的邊表結點的結構進行一些改造,就可以避免這個問題
重新定義的邊表結點結構如下所示
ivex | ilink | jvex | jlink |
---|
其中ivex和jvex是與某條邊依附的兩個頂點在頂點表中下標,ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊,這就是鄰接多重表結構
首先連線的①②③④就是將頂點的firstedge指向一條邊,頂點下標要與ivex的值相同,接着,由於頂點
鄰接多重表與鄰接表的差別,僅僅是在於同一條邊在鄰接表中用兩個節點表示,而在鄰接多重表中只有一個結點,這樣對邊的操作就方便多了,若刪除上圖的(
邊集數組
邊集數組是由兩個一維數組構成,一個是存儲頂點的信息;另一個是存儲邊的信息,這個邊數組每個數據元素由一條邊的起點下標(begin)、終點下標(end)和權(weight)組成
邊集數組關注的是邊的集合,在邊集數組中要查找一個頂點的度需要掃描整個邊數組,效率並不高,因此它更適合對邊依次進行處理操作,而不適合對頂點相關的操作
圖的遍歷
從圖中某一頂點出發訪遍圖中其餘頂點,且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)
深度優先遍歷
深度優先遍歷(Depth_First_Search),也稱爲深度優先搜索,簡稱DFS,類似於樹的前序遍歷
從頂點A開始,在沒有碰到重複頂點的情況下,始終是向右手邊走,當走到H處發現沒有通道沒走過,此時一層層向上返回,把沒有走過的通道標記,如D->I,直到返回頂點A
對於n個頂點e條邊的圖來說,鄰接矩陣由於是二維數組,要查找每個頂點的鄰接點需要訪問矩陣中的所有元素,因此需要O(
廣度優先遍歷
廣度優先遍歷(Breadth_First_Search),又稱廣度優先搜索,簡稱BFS,類似於樹的層序遍歷
先將下圖變形成層序結構,變形原則是頂點A放置在第一層,讓與它有邊的頂點B、F爲第二層,再讓與B和F有邊的頂點C、I、G、E爲第三層,再將這四個頂點有邊的D、H放在第四層
圖的深度優先遍歷與廣度優先遍歷算法在時間複雜度上是一樣的,不同之處在於對頂點訪問的順序不同
深度優先更適合目標比較明確,以找到目標爲主要目的的情況,而廣度優先更適合在不斷擴大遍歷範圍時找到相對最優解的情況
最小生成樹
把構造連通網的最小代價生成樹稱爲最小生成樹(Minimum Cost Spanning Tree)
找連通網的最小生成樹,經典的有兩種算法,普里姆算法和克魯斯卡爾算法
普里姆(Prim)算法
定義: 假設N=(P,{E})是連通網,TE是N上最小生成樹中邊的集合。算法從U={
,TE={}開始。重複執行下述操作:在所有u∈U,v∈V-U的邊(u,v)∈E中找一條代價最小的邊(
併入集合TE,同時
普里姆(Prim)算法是以某頂點爲起點,逐步找各頂點上最小權值的邊來構建最小生成樹的
此算法的時間複雜度爲O(
示例:
上圖中左圖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 */
}
}
}
}
最終構造過程如下圖所示
克魯斯卡爾(Kruskal)算法
定義: 假設N={V,{E}}是連通網,則令最小生成樹的初始狀態爲只有n個頂點而無邊的非連通圖T={V,{}},圖中每個頂點自成一個連通分量,在E中選擇代價最小的邊,若該邊依附的頂點落在T中不同的連通分量上,則將此邊加入到T中,否則捨去此邊而選擇下一條代價最小的邊。依次類推,直至T中所有頂點都在同一連通分量上爲止
克魯斯卡爾(Kruskal)算法是以邊爲目標去構建,因爲權值是在邊上,直接去找最小權值的邊來構建生成樹也是很自然的想法,只不過構建時要考慮是否會形成環路而已
示例:
上圖將左圖轉化成右圖的邊集數組,並且對它們按權值從小到大排序
克魯斯卡爾算法代碼如下:
/* 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(
對比兩個算法,克魯斯卡爾算法主要是針對邊展開,邊數少時效率會非常高,所以對於稀疏圖有很大的優勢;而普里姆算法對於稠密圖,即邊數非常多的情況會更好一些
最短路徑
在網圖和非網圖中,最短路徑的含義是不同的,由於非網圖它沒有邊上的權值,所謂的最短路徑,其實就是指兩頂點之間經過的邊數最少的路徑;而對於網圖來說,最短路徑,是指兩頂點之間經過的邊上權值之和最少的路徑,並且稱路徑上的第一個頂點是源點,最後一個頂點是終點
迪傑斯特拉(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(
弗洛伊德(Floyd)算法
如下圖,要求出所有頂點到所有頂點的最短路徑
先定義兩個二維數組
示例:
弗洛伊德算法如下:
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中的頂點序列
拓撲排序就是對一個有向圖構造拓撲序列的過程,構造時會有兩個結果,如果此網的全部頂點都被輸出,則說明它是不存在環(迴路)的AOV網;如果輸出頂點數少了,說明這個網存在環(迴路),不是AOV網
一個不存在迴路的AOV網,可以應用在各種各樣的工程或項目的流程圖中,滿足各種應用場景的需要
拓撲排序算法
對AOV網進行拓撲排序的基本思路是:從AOV網中選擇一個入度爲0的頂點輸出,然後刪除此頂點,並刪除以此頂點爲尾的弧,繼續重複此步驟,直到輸出全部頂點或者AOV網中不存在入度爲0的頂點爲止
先爲AOV網建立一個鄰接表,如下圖所示
拓撲排序算法實現如下:
/* 拓撲排序,若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的頂點,對該頂點對應的弧鏈表進行遍歷,將弧鏈表中的頂點的入度減一,不斷循環這個過程,最終輸出所有的頂點
對一個具有n個頂點e條弧的AOV網來說,將入度爲0的頂點入棧的時間複雜度爲O(n),而之後的while循環中,每個頂點進一次棧,出一次棧,入度減1的操作共執行了e次,所以整個算法的時間複雜度爲O(n+e)
關鍵路徑
在一個表示工程的帶權有向圖中,用頂點表示事件,用有向邊表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,稱爲AOE網(Activity On Edge Network)
AOE網中沒有入邊的頂點稱爲始點或源點,沒有出邊的頂點稱爲終點或匯點,正常情況下,AOE網只有一個源點一個匯點
AOV網是頂點表示活動的網,它只描述活動之間的制約關係,而AOE網是用邊表示活動的網,邊上的權值表示活動持續的時間,因此AOE網是要建立在活動之間制約關係沒有矛盾的基礎之上,再來分析完成整個工程至少需要多少時間,或者爲縮短完成工程所需時間,應當加快哪些活動等問題
把路徑上各個活動所持續的時間之和稱爲路徑長度,從源點到匯點具有最大長度的路徑叫做關鍵路徑,在關鍵路徑上的活動叫關鍵活動,如開始->發動機完成->部件集中到位->組裝完成就是關鍵路徑,路徑的長度爲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]相等來判斷
關鍵路徑算法
將AOE網轉化爲鄰接表結構,如下圖
求事件的最早發生時間etv的過程,就是從頭至尾找拓撲序列的過程,因此,在求關鍵路徑之前,需要先調用一次拓撲序列算法的代碼來計算etv和拓撲序列列表
由此可以得出計算頂點
在計算ltv時,其實是把拓撲序列倒過來進行的,因此可以得出計算頂點
求關鍵路徑的算法代碼如下:
/* 求關鍵路徑,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的數組求得如下圖
如果etv[1]=3,ltv[1]=7,表示的意思是如果時間單位是天的話,哪怕
最終的關鍵路徑如下圖所示
求關鍵路徑算法的時間複雜度爲O(n+e)