參考作者:Vamei 出處:http://www.cnblogs.com/vamei
圖是由節點和連接節點的邊構成的。節點之間可以由路徑,即邊的序列。根據路徑,可以從一點到達另一點。在一個復雜的圖中,圖中兩點可以存在許多路徑。最短路徑討論了一個非常簡單的圖論問題,圖中從A點到B點 ,那條路徑耗費最短?
這個問題又異常復雜,因為網絡的構成狀況可能很復雜。
一個最簡單的思路,是找出所有可能的從A到B的路徑,再通過比較,來尋找最短路徑。然而,這並沒有將問題簡化多少。因為搜索從A到B的路徑,這本身就是很復雜的事情。而我們在搜索所有路徑的過程中,有許多路徑已經繞了很遠,完全沒有搜索的必要。比如從上海到紐約的路線,完全沒有必要先從上海飛到南極,再從南極飛到紐約,盡管這一路徑也是一條可行的路徑。
所以,我們需要這樣一個算法:它可以搜索路徑,並當已知路徑包括最短路徑時,即停止搜索。我們先以無權網絡為例,看一個可行的最短路徑算法。
無權網絡
無權網絡(unweighted network)是相對於加權網絡的,這里的“權”是權重。每條邊的耗費相同,都為1。路徑的總耗費即為路徑上邊的總數。
我們用“甩鞭子”的方式,來尋找最短路徑。鞭子的長度代表路徑的距離。
手拿一個特定長度的鞭子,站在A點。甩出鞭子,能打到一些點。比如C和D。
將鞭子的長度增加1。再甩出鞭子。此時,從C或D出發,尋找距離為1的鄰接點,即E和F。這些點到A點的距離,為此時鞭子的長度。
記錄點E和F,並記錄它們的上游節點。比如E(C), F(D)。我們同樣可以記錄此時該點到A的距離,比如5。
如果要記錄節點E時,發現它已經出現在之前的記錄中,這說明曾經有更短的距離到E。此時,不將E放入記錄中。畢竟,我們感興趣的是最短路徑。如下圖中的E:
黃色的E不被記錄
最初的鞭子長度為0,站在A點,只能打到A點自身。當我們不斷增加鞭子長度,第一次可以打到B時,那么此時鞭子的長度,就是從A到B的最短距離。循着我們的記錄,倒推上游的節點,就可以找出整個最短路徑。
我們的記錄本是個很有意思的東西。某個點放入記錄時,此時的距離,都是A點到該點的最短路徑。根據記錄,我們可以反推出記錄中任何一點的最短路徑。這就好像真誠對待每個人。這能保證,當你遇到真愛時,你已經是在真誠相待了。實際上,記錄將所有節點分割成兩個世界:記錄內的,已知最短距離的;記錄外的,未知的。
加權網絡
在加權網絡中(weighted network),每條邊有各自的權重。當我們選擇某個路徑時,總耗費為路徑上所有邊的權重之和。
加權網絡在生活中很常見,比如從北京到上海,可以坐火車,也可以坐飛機。但兩種選擇耗費的時間並不同。再比如,我們打出租車和坐公交車,都可以到市區,但車資也有所不同。在計算機網絡中,由於硬件性能不同,連接的傳輸速度也有所差異。加權網絡正適用於以上場景。無權網絡是加權網絡的一個特例。
這個問題看起來和無權網絡頗為類似。但如果套用上面的方法,我們會發現,記錄中的節點並不一定是最短距離。我們看下面的例子:
很明顯,最短路徑是A->C->D->B,因為它的總耗費只有4。按照上面的方法,我們先將A放入記錄。從A出發,有B和C兩個如果將B和C同時放入記錄,那么記錄中的B並不符合最短距離的要求。
那么,為什么無權網絡可行呢?假設某次記錄時,鞭子長度為5,那么這次記錄點的鄰接點,必然是距離為6的點。如果這些鄰接點沒有出現過,那么6就是它們的最短距離。所有第一次出現的鄰接點,都將加入到下次的記錄中。比如下面的例子,C/D/E是到達A的鄰接點,它們到A的最短距離必然都是1。
對於加權網絡來說,即使知道了鄰接點,也無法判斷它們是否符合最短距離。在記錄C/D/E時,我們無法判斷未來是否存在如下圖虛線的連接,導致A的鄰接點E並不是下一步的最短距離點:
但情況並沒有我們想的那么糟糕。仔細觀察,我們發現,雖然無法一次判定所有的鄰接點為下一步的最短距離點,但我們可以確定點C已經處在從A出發的最短距離狀態。A到C的其它可能性,比如途徑D和E,必然導致更大的成本。
也就是說,鄰接點中,有一個達到了最短距離點,即鄰接點中,到達A距離最短的點,比如上面的C。我們可以安全的把C改為已知點。A和C都是已知點,點P成為新的鄰接點。P到A得距離為4。
出於上面的觀察,我們可以將節點分為三種:
- 已知點:已知到達A最短距離的點。“我是成功人士。”
- 鄰接點:有從記錄點出發的邊,直接相鄰的點。“和成功人士接觸,也有成功的機會哦。”
- 未知點:“還早得很。”
最初的已知點只有A。已知點的直接下游節點為鄰接點。對於鄰接點,我們需要獨立的記錄它們。我們要記錄的有:
- 當前情況下,從A點出發到達該鄰接點的最短距離。比如對於上面的點D,為6。
- 此最短距離下的上游節點。對於上面的點D來說,為A。
每次,我們將鄰接點中最短距離最小的點X轉為已知點,並將該點的直接下游節點,改為鄰接點。我們需要計算從A出發,經由X,到達這些新增鄰接點的距離:新距離 = X最短距離 + QX邊的權重。此時有兩種情況,
- 如果下游節點Q還不是鄰接點,那么直接加入,Q最短距離 = 新距離,Q上游節點為X。
- 如果下游節點Q已經是鄰接點,記錄在冊的上游節點為Y,最短距離為y。如果新距離小於y,那么最小距離改為新距離,上游節點也改為X。否則保持原記錄不變。
我們還用上面的圖,探索A到E的路徑:
第一步
狀態 | 已知距離 | 上游 | |
A | 已知 | 0 | A |
C | 鄰接 | 1 | A |
D | 鄰接 | 6 | A |
E | 鄰接 | 9 | A |
P | 未知 | 無窮 |
第二步
狀態 | 已知距離 | 上游 | |
A | 已知 | 0 | A |
C | 已知 | 1 | A |
D | 鄰接 | 6 | A |
E | 鄰接 | 9 | A |
P | 鄰接 | 4 | C |
第二步
狀態 | 已知距離 | 上游 | |
A | 已知 | 0 | A |
C | 已知 | 1 | A |
D | 鄰接 | 6 | A |
E | 鄰接 | 7 | P |
P | 已知 | 4 | C |
第三步
狀態 | 已知距離 | 上游 | |
A | 已知 | 0 | A |
C | 已知 | 1 | A |
D | 已知 | 6 | A |
E | 鄰接 | 7 | P |
P | 已知 | 4 | C |
最后,E成為已知。倒退,可以知道路徑為E, P, C, A。正過來,就是從A到E的最短路徑了。
上面的算法是經典的Dijkstra算法。本質上,每個鄰接點記錄的,是基於已知點的情況下,最好的選擇,也就是所謂的“貪婪算法”(greedy algorithm)。當我們貪婪時,我們的決定是臨時的,並沒有做出最終的決定。轉換某個點成為已知點后,我們增加了新的可能性,貪婪再次起作用。根據對比。隨后,某個鄰接點成為新的“貪無可貪”的點,即經由其它任意鄰接點,到達該點都只會造成更高的成本; 經由未知點到達該點更不可能,因為未知點還沒有開放,必然需要經過現有的鄰接點到達,只會更加繞遠。好吧,該點再也沒有貪婪的動力,就被扔到“成功人士”里,成為已知點。成功學不斷傳染,最后感染到目標節點B,我們就找到了B的最短路徑。
實現
理解了上面的原理,算法的實現是小菜一碟。我們借用圖 (graph)中的數據結構,略微修改,構建加權圖。
我們將上面的表格做成數組records,用於記錄路徑探索的信息。
重新給點A,C,D,E,P命名,為0, 1, 2, 3, 4。
代碼如下:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <limits.h> typedef struct Node { int element; int weight; struct Node *next; }node; typedef struct { int status; int distance; int previous; }record; void insertEdge(node *graph, int from, int to, int weight); //構建圖 void printGraph(node *graph, int n); //打印圖 void shortPath(node *graph, record *records, int n, int from, int to); //尋找最短路徑 int newNeighbors(node *graph, record *records, int n, int current); //尋找鄰接點 int main(void) { int n, m; int s, e, w; node *graph; record *records; scanf("%d%d", &n, &m); graph = (node *)calloc(n, sizeof(node)); records = (record *)calloc(n, sizeof(record)); for (int i = 1; i <= m; i++) { scanf("%d%d%d", &s, &e, &w); insertEdge(graph, s, e, w); } printGraph(graph, n); printf("\n0 -> 3:\n"); shortPath(graph, records, n, 0, 3); //尋找從0到3的最短路徑 system("pause"); return 0; } void insertEdge(node * graph, int from, int to, int weight) { node *add, *temp; temp = graph + from; add = (node *)calloc(1, sizeof(node)); add->element = to; add->weight = weight; add->next = temp->next; temp->next = add; } void printGraph(node * graph, int n) { node *temp; for (int i = 0; i < n; i++) { printf("%d :", i); temp = (graph + i)->next; while (temp) { printf("%d(%d) ", temp->element, temp->weight); temp = temp->next; } printf("\n"); } } void shortPath(node * graph, record * records, int n, int from, int to) { int current = from; for (int i = 0; i < n; i++) //初始化記錄表 { (records + i)->status = -1; (records + i)->distance = INT_MAX; (records + i)->previous = -1; } (records + from)->status = 1; //起點 (records + from)->distance = 0; (records + from)->previous = 0; while (current != to) { current = newNeighbors(graph, records, n, current); } while (current != from) { printf("%d<- ", current); current = (records + current)->previous; } printf("%d\n", current); } int newNeighbors(node * graph, record * records, int n, int current) { node *temp = (graph + current)->next; int element, distance; while (temp) //探索鄰接點 { element = temp->element; (records + element)->status = 0; distance = temp->weight + (records + current)->distance; if ((records + element)->distance > distance) { (records + element)->distance = distance; (records + element)->previous = current; } temp = temp->next; } int d = INT_MAX; for (int i = 0; i < n; i++) //下一個已知點 { if ((records + i)->status == 0 && (records + i)->distance < d) { d = (records + i)->distance; element = i; } } (records + element)->status = 1; return element; }
運行結果如下:
5 5
0 1 1
0 2 6
0 3 9
1 4 3
4 3 3
0 :3(9) 2(6) 1(1)
1 :4(3)
2 :
3 :
4 :3(3)
0 -> 3:
3<- 4<- 1<- 0
上面的算法中,最壞情況是目標節點最后成為已知點,即要尋找O(|V|)O(|V|)。而每個已知點是通過尋找O(|V|)O(|V|)個節點的最小值得到的。最后,打印出最短的路徑過程中,需要倒退,最多可能有O|E|O|E|,也就是說,算法復雜度為O(|V|2+|E|)O(|V|2+|E|)。
上面的records為一個數組,用於記錄路徑探索信息。我們可以用一個優先隊列來代替它,將已知的節點移除優先隊列。這樣可以達到更好的運算效率。
總結
最短路徑是尋找最優解的算法。在復雜的網絡中,簡單的實現方式無法運行,必須求助於精心設計的算法,比如這里的Dijkstra算法。利用貪婪的思想,我們不斷的優化結果,直到找到最優解。