最短路徑
在解決網路路由的問題中,尋找圖中一個頂點到另一個頂點的最短路徑或最小帶權路徑是非常重要的過程。
正式表述為,給定一個有向帶權圖G=(V,E),頂點s到V中頂點t的最短路徑為在E中邊的集合S中串連s到t代價最小的路徑。
當找到S時,我們就解決了單對頂點最短路徑問題。要做到這一點,實際上首先要解決更為一般的單源最短路徑問題,單源最短路徑問題是解決單對頂點最短路徑過程中的一部分。在單源最短路徑問題中,計算從一個頂點s到其他與之相鄰頂點之間的最短路徑。之所以要用這個方法解決此問題是因為沒有其他更好的辦法能用來解決單對頂點最短路徑問題。
Dijkstra演算法
解決單源最短路徑問題的方法之一就是Dijkstra演算法。
Dijkstra演算法會產生一棵最短路徑樹,樹的根為起始頂點s,樹的分支為從頂點s到圖G中所有其他頂點的最短路徑。此演算法要求圖中所有的權值均為非負數。與Prim演算法一樣,Dijkstra演算法也是一種利用貪心演算法計算並最終能夠產生最優結果的演算法。
從根本上說,Dijkstra演算法通過選擇一個頂點,並不斷地探索與此頂點相關的邊,以此來確定每個頂點的最短路徑最否是最優。此演算法類似廣度優先搜尋演算法,因為在往圖中更深的頂點探尋之前首先要遍曆與此頂點相關的所有頂點。為了計算s與其他所有頂點之前的最短路徑,Dijkstra演算法需要維護每個頂點的色值和最短路徑估計。通常,最短路徑估計由變數d表示。
開始,將所有色值設定為白色,最短路徑估計設定為∞(代表一個足夠大的數,大於圖中所有邊的權值)。將起始頂點的最短路徑估計設定為0。隨著演算法的不斷演化,在最短路徑樹中為每個頂點(除起始頂點)指派一個父結點。在演算法結束之前,頂點的父結點可能會發生幾次變化。
Dijkstra演算法的運行過程如下:
首先,在圖中所有白色頂點之間,選擇最短路徑估計值最小的頂點u。初始,最短路徑估計值被設定為0的頂點將做為起始頂點。當選擇此頂點後,將其塗成黑色。
接下來,對於每個與u相鄰的頂點v,釋放其邊(u,v)。當釋放邊後,我們要確認是否要更新到目前為止所計算的最短路徑。方法就是將(u,v)的權值加到u的最短路徑估計中。如果這個合計值小於或等於v的最短路徑估計,就將這個值指派給v,作為v的新最短路徑估計。同時,將u設定為v的父結點。
重複這個過程,直到所有的頂點都標記為黑色。一旦計算完最短路徑樹,那麼從s到另外一個頂點t的最短路徑就能唯一確定:從樹中t處的結點開始向隨後的父結點尋找,直到到達s。此尋找路徑的反向路徑即為s到t的最短路徑。
展示了由a到圖中其他頂點的最短路徑的計算過程。例如,a到b的最短路徑為(a,c,f,b),其權值為7。最短路徑估計和每個頂點的父結點都顯示在每個頂點的旁邊。最短路徑估計顯示在斜線的左邊,父結點顯示在斜線的右邊。淺灰色的邊是最短路徑樹增長過程中的邊。
最短路徑的介面定義
shortest
int shortest(Graph *graph, const PathVertex *start, List *paths, int (*match)(const void *key1, const void *key2));
傳回值:如果計算最短路徑成功,返回0;否則,返回-1。
描述:用於計算頂點start與有向帶權圖graph中其他所有頂點之間的最短路徑。此操作會改變graph,所以如果有必要,在調用此操作之前先對圖進行備份。
graph中的每個頂點必須包含PathVertex類型的資料。通過設定PathVertex結構體中的成員weight的值來指定每個邊的權值,weitht的值由傳入graph_ins_edge的參數data2決定。用PathVertex結構體的成員data來儲存與頂點相關的資料,例如頂點的標識符。
graph的match函數(此函數在用graph_init對圖進行初始化時調用)用來比較PathVertex結構體中的data成員。此函數與傳入shortest中的參數match相同。
一旦計算完成,最短路徑的相關資訊將會返回給paths,paths是儲存PathVertex結構體的列表。在paths中,起始頂點的父結點設定為NULL。而其他每個頂點的parents成員都指向位於該頂點之前的那個頂點,這個頂點位於從起始頂點開始的最短路徑之上。paths中的頂點指向graph中的實際頂點,所以只要能夠訪問paths,函數調用都就必須要保證graph中的記憶體空間有效。一旦不再使用paths,就調用list_destroy來銷毀paths。
複雜度:O(EV2),其中V是圖中的頂點個數,E是邊的條目數。
最短路徑的實現與分析
為了計算有向帶權圖中一個頂點到其他所有頂點的最短路徑,其圖的表示方法與最小產生樹中的表示方法相同。只是用PathVertex結構體取代頂點MstVertex結構。
PathVertex能夠表示帶樹圖,同時能夠追蹤Dijkstra演算法所需要的頂點和邊的資訊。此結構體包含5個成員:data是與頂點相關的資料;weight是到達該頂點的邊的權值;color是頂點的顏色;d是頂點的最短路徑估計;parent是最短路徑中頂點的交結點。
構造一個包含pathvertex結構體的圖的過程與構造一個包含MstVertex結構體的圖的過程相同。
shortest操作首先初始化鄰接表結構鏈表中的每個頂點。將每個頂點的最短路徑估計初始化為DBL_MAX(起始頂點除外,起始頂點的初始值為0.0)。用儲存在鄰接表結構鏈表中的頂點來維護頂點的色值、最短路徑估計和父結點。其原因與計算最小產生樹時的解釋相同。
Dijkstra演算法的核心是用一個單迴圈為圖中的每個結點迭代一次。在每次的迭代過程中,首先在所選的白色頂點中選擇最短路徑估計最小的頂點。同時,在鄰接表結構鏈表中將此頂點塗黑。接下來,遍曆與所選頂點相鄰的頂點。在遍曆每個頂點時,檢查它在鄰接表結構鏈表中的顏色和最短路徑估計。一旦獲得了這些資訊,就調用relax釋放所選頂點與相鄰頂點間的邊。如果此過程中發現需要更新相鄰頂點的最短路徑估計和父結點,那麼就在鄰接表結構鏈表中更新此頂點。重複這個過程直到所有頂點都塗成黑色。
一旦Dijkstra演算法中的主迴圈結束,計算圖中起始頂點到所有其他頂點的最短路徑的過程也就完成了。此時,將鄰接表結構鏈表中每個黑色的PathVertex結構體插入鏈表paths中。在paths中,父結點為NULL的頂點就是起始頂點。其他每個頂點的parent成員都指向從起始頂點開始的最短路徑中的前一個頂點。每個PathVertex結構體的成員weight並不經常使用,因為它只在儲存到鄰接表中時才用的到。展示了在1中計算最短路徑時所返回的PathVertex結構體列表。
樣本:計算最短路徑的實現
/*shortest.c*/#include <float.h>#include <stdlib.h>#include "graph.h"#include "graphalg.h"#include "list.h"#include "set.h"/*relax 釋放邊、更新最短路徑估計值、更新父結點*/static void relax(PathVertex *u, PathVertex *v, double weight){ /*釋放頂點u和v之間的邊*/ if(v->d > u->d + weight) { v-> = u->d + weight; v->parent = u; } return;}/*shortest 最短路徑計算函數*/int shortest(Graph *graph, const PathVertex *start, List *paths, int (*match)(const void *key1, const void key2)){ AdjList *adjlist; PathVertex *pth_vertex, *adj_vertex; ListElmt *element, member; double minimum; int found,i; /*初始化圖中的所有頂點*/ found = 0; for(element = list_head(&graph_adjlists(graph)); element != NULL; element = list_next(element)) { pth_Vertex = ((AdjList *)list_data(element))->vertex; if(match(pth_vertex, start)) { /*找到並初始化起始頂點*/ pth_vertex->color = white; pth_vertex->d = 0; pth_vertex->parent = NULL; found = 1; } else { /*非起始頂點,初始化*/ pth_vertex->color = white; pth_vertex->d = DBL_MAX; pth_vertex->parent = NULL; } } /*如果未匹配到起始頂點,程式退出*/ if(!found) return -1; /*使用Dijkstra演算法計算從起始頂點開始的最短路徑*/ i=0; while(i < graph_vcount(graph)) { /*從所有的白色頂點中,選擇最短路徑估計值最小的頂點*/ minimum = DBL_MAX; for(element=list_head(&graph_adjlists(graph)); element!=NULL; element = list_next(element)) { pth_vertex = ((AdjList*)list_data(element))->vertex; if(pth_vertex->color == white && pth_vertex->d < minimum) { minimum = pth_vertex->d; adjlist = list_data(element); } } /*將該頂點塗成黑色*/ ((PathVertex *)adjlist->vertex)->color = black; /*遍曆與所選頂點相鄰的頂點*/ for(member=list_head(&adjlist->adjacent); member != NULL; member = list_next(member)) { adj_vertex = list_data(member); for(element = list_head(&graph_adjlists(graph)); element != NULL; element = list_next(element)) { pth_vertex = ((AdjList *)list_data(element))->vertex; if(match(pth_vertex, adj_vertex)) { relax(adjlist->vertex, pth_vertex, adj_vertex->weight); } } } i++; } /*將鄰接表結構鏈表中每個黑色PathVertexx結構體插入鏈表paths中*/ list_init(paths,NULL); for(element=list_head(&graph_adjlists(graph)); element!=NULL; element=list_next(paths,NULL)) { /*載入鄰接表結構鏈表中的每一個黑色頂點*/ pth_vertex=((AdjList *)list_data(element))->vertex; if(pth_vertex->color == black) { if(list_ins_next(paths, list_tali(paths), pth_vertex) != 0) { list_destroy(paths); return -1; } } } return 0;}
最短路徑執行個體:路由表
最短路徑演算法在現實中一個很重要的應用是在互連網中對資料進行路由。路由是將資料從一個點傳輸到另一個點的決策過程。在互連網中,路由是沿著相互串連的點(稱為網關)傳播資料區段或資料包的過程。在資料包通過一個網關時,路由器將會查看資料包最終目的地,然後將資料包發往下一個網關。路由器的目的就是將資料包往最接近於目的地的地方發送。
為了將資料包往最接近目的地的地方發送,每個路由器都要維護互連網的結構資訊或拓撲資訊。這些資訊儲存在路由表中。路由表為每個路由器知道如何到達的網關儲存一個條目。每個條目指定把資料發送到下一個網關的地址。
由於路由器會周期性的隨著互連網的變化更新其路由表,因此資料包會儘可能地沿著最佳路徑傳送資料。有一種類型的路由稱為最短路徑優先路由或SPF路由,其中每個路由器都維護有自己的網狀圖,以便它能通過計算自身與其他結點之間的最短路徑來更新其路由表。互連網拓撲圖是一個有向帶權圖,其頂點為網關,邊為網關之間的連接線。邊的權值由串連路徑的效能決定。偶爾,路由器會交換拓撲和效能的資訊,為此還專門設計了一種協議來完成此工作。
設計函數route,利用SPF路由演算法計算更新路由表中條目所需要的資訊。
該函數接受shortest的paths參數中返回的路徑資訊列表。
它使用此資訊來確定路由器要把資料包發送到的下一個網關,以保證此網關離目的地更近了一步。
要為指定的網關完成一個完整的表,首先要調用函數shortest,其中網關由start參數傳入。
接著,對於每個路由表中包含的目的地址,調用函數route,其中的目的地址由destination傳入。
然後作為從paths產生的路徑的圖graph_init中所提供的match函數,把此地址傳入match中。
route函數將目的列表paths中的父結點指標指向網關,同時返回一個傳送資料包的最佳結點(此結點存放在輸出參數next中)。next中返回的頂點指向paths中實際的頂點,所以只要還能訪問next,paths中的記憶體空間就必須有效。
A部分展示了互連網中處於a的路由器的路由表的計算過程。B部分展示了處理b的路由器計算路由表的過程。注意,依據在互連網中起始位置的不同,其最短路徑也不同。同樣需要注意的是,在圖B中是沒辦法到達a的,所以在該表中也沒有相關條目。
計算路由的時間複雜度為O(n2),其中n為paths中的網關數目。
樣本:路由表中更新條目的函數實現
/*route.c*/#include <stdlib.h>#include "graphalg.h"#include "list.h"#include "route.h"/*route*/int route(List *paths, PathVertex *destination, PathVertex **next, int (*match)(const void *key1, const void *key2)){ PathVertex *temp, *parent; ListElmt *element; int found; /*尋找位於網關鏈表中的目的地址*/ found = 0; for(element = list_head(paths); element != NULL; element = list_next(element)) { if(match(list_data(element),destination)) { temp = list_data(element); parent = ((PathVertex *)list_data(element))->parent; found = 1; break; } } /*如未發現目標地址,函數退出*/ if(!found) return -1; /*計算到目的地最短路徑的下一個網關*/ while(parent!=NULL) { temp = list_data(element); found = 0; for(element = list_head(paths); element != NULL; element = list_next(element)) { if(match(list_data(element),parent)) { parent = ((PathVertex *)list_data(element))->parent; found = 1; break; } } /*如果目標不能到達,函數退出*/ if(!found) return -1; } *next = temp; return 0;}