最短路徑
在解決網絡路由的問題中,尋找圖中一個頂點到另一個頂點的最短路徑或最小帶權路徑是非常重要的過程。
正式表述為,給定一個有向帶權圖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; }