路径算法

转自:http://www.csie.ntnu.edu.tw/~u91029/Path.html

把一張圖想像成道路地圖,把圖上的點想像成地點,把圖上的邊想像成道路,把權重想像成道路的長度。若兩點之間以邊相連,表示兩個地點之間有一條道路,道路的長度是邊的權重。

有時候為了應付特殊情況,邊的權重可以是零或者負數,也不必真正照著圖上各點的地理位置來計算權重。別忘記「圖」是用來記錄關聯的東西,並不是真正的地圖。

Walk / Circuit

在圖上任取兩點,分別作為起點和終點,我們可以規劃出許多條由起點到終點的路線。這些路線可以經過其他點,也可以來來回回的繞圈子。一條路線,就是一條「途徑」。

如果起點到終點是不相通的,那麼就不會存在起點到終點的途徑。如果起點和終點一樣,那麼就存在途徑,途徑是一個點、零條邊。

途徑也有權重。途徑經過的每一條邊,沿路加總權重,就是途徑的權重(通常只加總邊的權重,而不考慮點的權重)。途徑的權重,可以想像成途徑的總長度。

至於頭尾相接的途徑則稱作「回路」。

Trail / Circuit

一條途徑,沒有重複地經過同樣的邊,就稱做「跡」。

至於頭尾相接的跡,沒有特別命名,可稱作「回路」。

Path / Cycle

一條途徑,沒有重複地經過同樣的點(與邊),就稱做「路徑」。

至於頭尾相接的路徑則稱作「環」。

【註:關於這些名詞,每個人的定義方式都略有差異,詳見http://planetmath.org/encyclopedia/OpenWalk.html

Shortest Path

程度★ 難度★★

Shortest Walk

「最短途徑」是兩點之間權重最小的途徑。最短途徑不見得是邊最少、點最少的途徑。

「最短途徑」也可能不存在。兩點之間不連通、不存在途徑的時候,就沒有最短途徑。

Shortest Path

「最短路徑」和最短途徑相仿,差異在於路徑不可重複經過同樣的點和邊。

Shortest Walk 與 Shortest Path

權重為負值的環,稱作「負環( Negative Cycle )」。

當一張圖有負環,只要不斷去繞行負環,「最短途徑」的長度將是無限短。

當一張圖沒有負環,「最短途徑」等於「最短路徑」。

一條途徑重複經過同一條邊、同一個點,一定會讓途徑變長。由此可知:沒有負環的情況下,「最短途徑」等於「最短路徑」,決不會經過同樣的邊、同樣的點。

當一張圖有負環時,最短途徑無限短,我們不必再討論;當一張圖沒有負環時,最短途徑就是最短路徑,我們可以專心討論路徑、而非途徑。

Shortest Path Tree

在圖上選定一個起點,由起點到圖上各點的最短路徑們,形成一棵有向樹,稱作「最短路徑樹」。由於最短路徑不見得只有一條,所以固定起點的最短路徑樹也不見得只有一種。

最短路徑樹上的每一條最短路徑,都是由其它的最短路徑延展而得;截去末端之後,還是最短路徑。

Shortest Path Graph 【尚無正式稱呼】

在圖上選定一個起點和終點,由起點到終點的所有最短路徑們,形成一張有向圖,稱作「最短路徑圖」,只有唯一一種。

當圖上每一條邊的權重都是正數,最短路徑圖是有向無環圖( Directed Acyclic Graph, DAG )。

兩點之間有多條邊

當一張圖的兩點之間有多條邊,可以留下一條權重最小的邊。這麼做不影響最短路徑。

兩點之間沒有邊(兩點不相鄰)

當一張圖的兩點之間沒有邊,可以補上一條權重無限大的邊。這麼做不影響最短路徑。

當圖的資料結構為 adjacency matrix 時,任兩點之間都一定要有一個權重值。要找最短路徑,不相鄰的兩點之間,權重值必須設定為一個超大數字,當作無限大;不可設定為零,以免計算錯誤。

最短路徑無限長、無限短

當起點無法到達終點,就沒有最短路徑了。這種情況常被解讀成:起點永遠走不到終點,導致最短路徑無限長。

當圖上有負環,不斷去繞行負環,導致最短路徑無限短。

Relaxation

最後介紹最短路徑演算法一個共通的重要概念「鬆弛」。

尋找兩點之間的最短路徑時,最直觀的方式莫過於:先找一條路徑,然後再找其他路徑,看看會不會更短,並記住最短的一條。

找更短的路徑並不困難。我們可以尋覓捷徑,以縮短路徑;也可以另闢蹊徑,取代原本的路徑。如此找下去,必會找到最短路徑。

尋覓捷徑、另闢蹊徑的過程,可以以數學方式來描述:現在要找尋起點為 s 、終點為 t 的最短路徑,而且現在已經有一條由 s 到 t 的路徑,這條路徑上會依序經過 a 及 b 這兩點(可以是起點和終點)。我們可以找到一條新的捷徑,起點是 a 、終點是 b 的捷徑,以這條捷徑取代原本由 a 到 b 的這一小段路徑,讓路徑變短。

找到捷徑以縮短原本路徑,便是 Relaxation 。

附錄

最短路徑演算法的功能類型:

Point-to-Point Shortest Path,點到點最短路徑:
給定起點、終點,求出起點到終點的最短路徑。一對一。

Single Source Shortest Paths,單源最短路徑:
給定起點,求出起點到圖上每一點的最短路徑。一對全。

All Pairs Shortest Paths,全點對最短路徑:
求出圖上所有兩點之間的最短路徑。全對全。

有向圖、最短路徑演算法的原理:

Label Setting:
逐步設定每個點的最短路徑長度,一旦設定後就不再更改。
負邊不適用。

Label Correcting:
設定某個點的最短路徑長度之後,之後仍可繼續修正,越修越美。
整個過程就是不斷重新標記每個點的最短路徑長度。
負邊適用。

無向圖、最短路徑演算法的原理:

當無向圖沒有負邊,尚可套用有向圖的演算法。
當無向圖有負邊,則必須使用「T-Join」。

問題複雜度:

最短途徑:P問題。
最短路徑:NP-Complete問題;當圖上沒有負環,才是P問題。
最長途徑:每一條邊的權重添上負號,就變成最短途徑問題。
最長路徑:每一條邊的權重添上負號,就變成最短路徑問題。

古代人把walk叫做path、把path叫做simple path。
早期文獻說shortest path是P問題,
純粹是因為古代人與現代人用了不同的名詞定義。

Single Source Shortest Paths:
Label Setting Algorithm

程度★ 難度★★

用途

一張有向圖,選定一個起點,求出起點到圖上各點的最短路徑,即是最短路徑樹。但是限制是:圖上每一條邊的權重皆非負數。

想法

當圖上每一條邊的權重皆非負數時,可以發現:每一條最短路徑,都是邊數更少、權重更小(也可能相同)的最短路徑的延伸。

於是乎,建立最短路徑樹,可以從邊數最少、權重最小的最短路徑開始建立,然後逐步延伸拓展。換句話說,就是從距離起點最近的點和邊開始找起,然後逐步延伸拓展。先找到的點和邊,保證會是最短路徑樹上的點和邊。

也可以想成是,從目前形成的最短路徑樹之外,屢次找一個離起點最近的點,(連帶著邊)加入到最短路徑樹之中,直到圖上所有點都被加入為止。

整個演算法的過程,可看作是兩個集合此消彼長。不在樹上、離根最近的點,移之。

運用已知的最短路徑,求出其他的最短路徑。循序漸進、保證最佳,這是Greedy Method的概念。

演算法

1. 將起點加入到最短路徑樹。此時最短路徑樹只有起點。
2. 重複下面這件事V-1次,將剩餘所有點加入到最短路徑樹。
 甲、尋找一個目前不在最短路徑樹上而且離起點最近的點b。
 乙、將b點加入到最短路徑樹。

運用Memoization,建立表格紀錄最短路徑長度,便容易求得不在樹上、離根最近的點。時間複雜度是O(V^3)。

令w[a][b]是a點到b點的距離(即是邊的權重)。
令d[a]是起點到a點的最短路徑長度,起點設為零,其他點都是空的。

1. 將起點加入到最短路徑樹。此時最短路徑樹只有起點。
2. 重複下面這件事V-1次,將剩餘所有點加入到最短路徑樹。
 甲、尋找一個目前不在最短路徑樹上而且離起點最近的點:
   以窮舉方式,
   找一個已在最短路徑樹上的點a,以及一個不在最短路徑樹上的點b,
   讓d[a]+w[a][b]最小。
 乙、將b點的最短路徑長度存入到d[b]之中。
 丙、將b點(連同邊ab)加入到最短路徑樹。

實作

一點到多點的最短路徑、找出最短路徑樹(adjacency matrix)
  1. int w[9][9];    // 一張有權重的圖:adjacency matrix
  2. int d[9];       // 紀錄起點到圖上各個點的最短路徑長度
  3. int parent[9];  // 紀錄各個點在最短路徑樹上的父親是誰
  4. bool visit[9];  // 紀錄各個點是不是已在最短路徑樹之中
  5. void label_setting(int source)
  6. {
  7.     for (int i=0i<100i++) visit[i] = false// initialize
  8.     d[source] = 0;              // 設定起點的最短路徑長度
  9.     parent[source] = source;    // 設定起點是樹根(父親為自己)
  10.     visit[source] = true;       // 將起點加入到最短路徑樹
  11.     for (int k=0k<9-1k++)   // 將剩餘所有點加入到最短路徑樹
  12.     {
  13.         // 從既有的最短路徑樹,找出一條聯外而且是最短的邊
  14.         int a = -1b = -1min = 1e9;
  15.         // 找一個已在最短路徑樹上的點
  16.         for (int i=0i<9i++)
  17.             if (visit[i])
  18.                 // 找一個不在最短路徑樹上的點
  19.                 for (int j=0j<9j++)
  20.                     if (!visit[j])
  21.                         if (d[i] + w[i][j] < min)
  22.                         {
  23.                             a = i;  // 記錄這一條邊
  24.                             b = j;
  25.                             min = d[i] + w[i][j];
  26.                         }
  27.         // 起點有連通的最短路徑都已找完
  28.         if (a == -1 || b == -1break;
  29. //      // 不連通即是最短路徑長度無限長
  30. //      if (min == 1e9) break;
  31.         d[b] = min;         // 儲存由起點到b點的最短路徑長度
  32.         parent[b] = a;      // b點是由a點延伸過去的
  33.         visit[b] = true;    // 把b點加入到最短路徑樹之中
  34.     }
  35. }

Graph Traversal

Label Setting Algorithm亦可看做是一種Graph Traversal,遍歷順序是先拜訪離樹根最近的點和邊。

Single Source Shortest Paths:
Dijkstra's Algorithm

程度★ 難度★★★

想法

找不在樹上、離根最近的點,先前的方式是:窮舉樹上a點及非樹上b點,找出最小的d[a]+w[a][b]。整個過程重覆窮舉了許多邊。

表格改為儲存d[a]+w[a][b],就不必重覆窮舉邊了。每當一個a點加入最短路徑樹,就將d[a]+w[a][b]存入d[b]。找不在樹上、離根最近的點,就直接窮舉d[]表格,找出最小的d[b]。

演算法

令w[a][b]是a點到b點的距離(即是邊的權重)。
令d[a]是起點到a點的最短路徑長度,起點設為零,其他點都設為無限大。

1. 重複下面這件事V次,以將所有點加入到最短路徑樹。
 甲、尋找一個目前不在最短路徑樹上而且離起點最近的點:
   直接搜尋d[]陣列裡頭的數值,來判斷離起點最近的點。
 乙、將此點加入到最短路徑樹之中。
 丙、令剛剛加入的點為a點,
   以窮舉方式,找一個不在最短路徑樹上、且與a點相鄰的點b,
   把d[a]+w[a][b]存入到d[b]當中。
   因為要找最短路徑,所以儘可能紀錄越小的d[a]+w[a][b]。
   (即是邊ab進行relaxation)

以Relaxation的角度來看,此演算法不斷以邊ab做為捷徑,讓起點到b點的路徑長度縮短為d[a]+w[a][b]。

時間複雜度

分為兩個部分討論:

甲、加入點、窮舉邊:每個點只加入一次,每條邊只窮舉一次,剛好等同於一次Graph Traversal的時間。

乙、尋找下一個點:從大小為V的陣列當中尋找最小值,為O(V);總共尋找了V次,為O(V^2)。

甲乙相加就是整體的時間複雜度。圖的資料結構為adjacency matrix的話,便是O(V^2);圖的資料結構為adjacency lists的話,還是O(V^2)。

實作

找出最短路徑樹(adjacency matrix)
  1. int w[9][9];    // 一張有權重的圖
  2. int d[9];       // 紀錄起點到各個點的最短路徑長度
  3. int parent[9];  // 紀錄各個點在最短路徑樹上的父親是誰
  4. bool visit[9];  // 紀錄各個點是不是已在最短路徑樹之中
  5. void dijkstra(int source)
  6. {
  7.     for (int i=0i<9i++) visit[i] = false;   // initialize
  8.     for (int i=0i<9i++) d[i] = 1e9;
  9.     d[source] = 0;
  10.     parent[source] = source;
  11.     for (int k=0k<9k++)
  12.     {
  13.         int a = -1b = -1min = 1e9;
  14.         for (int i=0i<9i++)
  15.             if (!visit[i] && d[i] < min)
  16.             {
  17.                 a = i;  // 記錄這一條邊
  18.                 min = d[i];
  19.             }
  20.         if (a == -1break;     // 起點有連通的最短路徑都已找完
  21. //      if (min == 1e9) break;  // 不連通即是最短路徑長度無限長
  22.         visit[a] = true;
  23.         // 以邊ab進行relaxation
  24.         for (b=0b<9b++)
  25.             if (!visit[b] && d[a] + w[a][b] < d[b])
  26.             {
  27.                 d[b] = d[a] + w[a][b];
  28.                 parent[b] = a;
  29.             }
  30.     }
  31. }
從最短路徑樹上找出最短路徑(adjacency matrix)
  1. // 若要找出某一點的最短路徑,就可以利用parent陣列了。
  2. void find_path(int x)   // 印出由起點到x點的最短路徑
  3. {
  4.     if (x != parent[x]) // 先把之前的路徑都印出來
  5.         find_path(parent[x]);
  6.     cout << x << endl;  // 再把現在的位置印出來
  7. }
找出最短路徑樹(adjacency lists)
  1. struct Element {int bw;} lists[9];    // 一張有權重的圖
  2. int size[9];
  3. int d[9];
  4. int parent[9];
  5. bool visit[9];
  6. void dijkstra(int source)
  7. {
  8.     for (int i=0i<9i++) visit[i] = false;
  9.     for (int i=0i<9i++) d[i] = 1e9;
  10.     d[source] = 0;
  11.     parent[source] = source;
  12.     for (int k=0k<9k++)
  13.     {
  14.         int a = -1b = -1min = 1e9;
  15.         for (int i=0i<100i++)
  16.             if (!visit[i] && d[i] < min)
  17.             {
  18.                 a = i;
  19.                 min = d[i];
  20.             }
  21.         if (a == -1break;
  22.         visit[a] = true;
  23.         for (int i=0i<size[a]; i++)
  24.         {
  25.             int b = lists[a][i].bw = lists[a][i].w;
  26.             if (!visit[b] && d[a] + w < d[b])
  27.             {
  28.                 d[b] = d[a] + w;
  29.                 parent[b] = a;
  30.             }
  31.         }
  32.     }
  33. }
找出最短路徑樹(edge list)
  1. struct Edge {int abw;}; // 紀錄一條邊的資訊
  2. Edge edges[13];
  3. int d[9];
  4. int parent[9];
  5. bool visit[9];
  6. void dijkstra(int source)
  7. {
  8.     for (int i=0i<9i++) visit[i] = false;
  9.     for (int i=0i<9i++) d[i] = 1e9;
  10.     d[source] = 0;
  11.     parent[source] = source;
  12.     for (int k=0k<9k++)
  13.     {
  14.         int a = -1b = -1min = 1e9;
  15.         for (int i=0i<100i++)
  16.             if (!visit[i] && d[i] < min)
  17.             {
  18.                 a = i;
  19.                 min = d[i];
  20.             }
  21.         if (a == -1break;
  22.         visit[a] = true;
  23.         for (int i=0i<13i++)
  24.             if (edges[i].a == a)
  25.             {
  26.                 int b = edges[i].bw = edges[i].w;
  27.                 if (!visit[b] && d[a] + w < d[b])
  28.                 {
  29.                     d[b] = d[a] + w;
  30.                     parent[b] = a;
  31.                 }
  32.             }
  33.     }
  34. }

延伸閱讀:Fibonacci Heap

用特殊的資料結構可以加快這個演算法。建立V個元素的Fibonacci Heap,用其decrease key函式來實作relaxation,用其extract min函式來找出下一個點,可將時間複雜度降至O(E+VlogV)。

UVa 10801 10841 10278 10187 10039

Single Source Shortest Paths:
Label Setting Algorithm + Priority Queue

程度★★ 難度★

演算法

找不在樹上、離根最近的點,先前的方式是:窮舉樹上a點及非樹上b點,也就是窮舉從樹上到非樹上的邊ab,以找出最小的d[a]+w[a][b]。

現在把d[a]+w[a][b]的值通通倒進Priority Queue。找不在樹上、離根最近的點,就從Priority Queue取出邊(與點);每次relaxation就將邊(與點)塞入Priority Queue。

學過State Space Search的讀者,可以發現此演算法正是Uniform-cost Search,因此也有人說此演算法是考慮權重的BFS。

找出最短路徑樹(adjacency matrix)
  1. // 要丟進Priority Queue的邊。
  2. // ab是邊,d是起點到b點可能的最短路徑長度。
  3. struct Edge {int abd;};
  4. // C++ STL內建的Priority Queue是Max-Heap,
  5. // 而不是Min-Heap,故必須改寫一下比大小的函式。
  6. bool operator<(const Edgee1const Edgee2)
  7. {
  8.     return e1.d > e2.d;
  9. }
  10. int w[9][9];
  11. int d[9];
  12. int parent[9];
  13. bool visit[9];
  14. void label_setting_with_priority_queue(int source)
  15. {
  16.     for (int i=0i<9i++) visit[i] = false;
  17.     for (int i=0i<9i++) d[i] = 1e9;
  18.     // C++ STL的Priority Queue。
  19.     priority_queue<EdgePQ;
  20.     int a = source;
  21.     d[a] = 0;
  22.     parent[a] = 0;
  23.     visit[a] = true;
  24.     for (int i=0i<9-1i++)
  25.     {
  26.         // 比大小的工作,交由Priority Queue處理。
  27.         for (int b=0b<9b++)
  28.             if (!visit[b])
  29.                 PQ.push( (Edge){abd[a] + w[a][b]} );
  30.         // 找出下一個要加入到最短路徑樹的邊(與點)
  31.         Edge e = (Edge){-1, -10};
  32.         while (!PQ.empty())
  33.         {
  34.             e = PQ.top();   PQ.pop();
  35.             if (!visit[e.b]) break;
  36.         }
  37.         // 起點有連通的最短路徑都已找完
  38.         if (e.a == -1 || e.b == -1break;
  39.         a = e.b;
  40.         d[a] = e.d;
  41.         parent[a] = e.a;
  42.         visit[a] = true;
  43.     }
  44. }

時間複雜度:維護Priority Queue

首先必須確認Priority Queue的大小。圖上每一條邊皆用於relaxation一次,所以Priority Queue前前後後一共塞入了E條邊,最多也只能取出E條邊。Priority Queue的大小為O(E)。

塞入一條邊皆需時O(logE),塞入E條邊皆需時O(ElogE)。取出亦如是。由此可知維護Priority Queue需時O(ElogE)。

在最短路徑問題當中,如果兩點之間有多條邊,只要取權重比較小的邊來進行最短路徑演算法就行了。也就是說,兩點之間只會剩下一條邊。也就是說,邊的總數不會超過C{V,2} = V*(V-1)/2個。也就是說,上述的時間複雜度O(ElogE),得改寫成O(Elog(V^2)) = O(2ElogV) = O(ElogV)。

Priority Queue可以採用Binary Heap或Binomial Heap,時間複雜度都相同。

當圖上每條邊的權重皆為正整數的情況下,Priority Queue亦得採用vEB Tree,時間複雜度下降成O(EloglogW),其中W為最長的最短路徑長度。

時間複雜度

一次Graph Traversal的時間,加上維護Priority Queue的時間。

圖的資料結構為adjacency matrix的話,便是O(V^2 + ElogE);圖的資料結構為adjacency lists的話,便是O(V+E + ElogE)。

這個方法適用於圖上的邊非常少的情況。若是一般情況,使用Dijkstra's Algorithm會比較有效率,程式碼的結構也比較簡單。

Single Source Shortest Paths:
Dijkstra's Algorithm + Priority Queue

程度★★ 難度★

演算法

時間複雜度與上一篇文章相同,然而效率較佳。

令w[a][b]是a點到b點的距離(即是邊的權重)。
令d[a]是起點到a點的最短路徑長度,起點設為零,其他點都是空的。
令PQ是一個存放點的Priority Queue,由小到大排序鍵值。

1. 把起點放入PQ。
2. 重複下面這件事,直到最短路徑樹完成為止:
 甲、嘗試從PQ中取出一點a,點a必須是目前不在最短路徑樹上的點。
 乙、將a點(連同其邊)加入最短路徑樹。
 丙、將所有與a點相鄰且不在樹上的點的點b(連同邊ab)放入PQ,
   設定鍵值為d[a] + w[a][b],鍵值同時也存入d[b],
   但是會先檢查d[a] + w[a][b]是不是大於d[b],
   大於才放入PQ,鍵值才存入d[b]。
   (此步驟即是以邊ab進行ralaxation。)
找出最短路徑樹(adjacency matrix)
  1. // 要丟進Priority Queue的點
  2. // b是點,d是可能的最短路徑長度。
  3. // a可以提出來,不必放在Node裡。
  4. struct Node {int bd;};
  5. bool operator<(const Noden1const Noden2) {return n1.d > n2.d;}
  6. int w[9][9];
  7. int d[9];
  8. int parent[9];
  9. bool visit[9];
  10. void dijkstra_with_priority_queue(int source)
  11. {
  12.     for (int i=0i<9i++) visit[i] = false;
  13.     for (int i=0i<9i++) d[i] = 1e9;
  14.     // C++ STL的Priority Queue
  15.     priority_queue<NodePQ;
  16.     d[source] = 0;
  17.     parent[source] = source;
  18.     PQ.push((Node){sourced[source]});
  19.     for (int i=0i<9i++)
  20.     {
  21.         // 找出下一個要加入到最短路徑樹的點。
  22.         int a = -1;
  23.         while (!PQ.empty() && visit[a = PQ.top().b])
  24.             PQ.pop();   // 最後少pop一次,不過無妨。
  25.         if (a == -1break;
  26.         visit[a] = true;
  27.         for (int b=0b<9b++)
  28.             if (!visit[b] && d[a] + w[a][b] < d[b])
  29.             {
  30.                 d[b] = d[a] + w[a][b];
  31.                 parent[b] = a;
  32.                 // 交由Priority Queue比較大小
  33.                 PQ.push( (Node){bd[b]} );
  34.             }
  35.     }
  36. }

UVa 10278 10740 10986

Single Source Shortest Paths:
Dial's Algorithm

程度★★ 難度★

演算法

用Bucket Sort代替表格,把d[a]+w[a][b]的值通通拿去做Bucket Sort。用在每條邊的權重都是非負整數的圖。

找出最短路徑樹(adjacency matrix)
  1. int w[9][9];
  2. int d[9];
  3. int parent[9];
  4. bool visit[9];
  5. // 建立500個bucket,每個bucket是一個queue。
  6. queuepair<int,int> > bucket[500];
  7. void dial(int source)
  8. {
  9.     for (int i=0i<9i++) visit[i] = false;
  10.     for (int i=0i<9i++) d[i] = 1e9;
  11.     for (int i=0i<500i++) bucket[i].clear();
  12.     bucket[0].pushmake_pair(sourcesource) );
  13.     for (int k=0slot=0k<9 && slot<500k++)
  14.     {
  15.         while (slot < 500 && bucket[slot].empty()) slot++;
  16.         if (slot == 500break// 起點有連通的最短路徑都已找完
  17.         int a = bucket[slot].front().first;
  18.         parent[a] = bucket[slot].front().second;
  19.         d[a] = slot;
  20.         visit[a] = true;
  21.         bucket[slot].pop();
  22.         for (int b=0b<9b++)
  23.             if (!visit[b])
  24.             {
  25.                 int s = d[a] + w[a][b];
  26.                 bucket[s].pushmake_pair(ba) );
  27.             }
  28.     }
  29. }

時間複雜度:進行Bucket Sort

整個bucket最多放入E個點、拿出E個點,然後整個bucket讀過一遍,時間複雜度總共是O(E+W),其中W為bucket的數目,也是最長的最短路徑長度。

當圖上每條邊的權重不是整數時,時間複雜度是O(WV)。

時間複雜度

一次Graph Traversal的時間,再加上Bucket Sort的時間。

圖的資料結構為adjacency matrix的話,便是O(V^2 + W);圖的資料結構為adjacency lists的話,便是O(V+E + W)。

當圖上每條邊的權重不是整數時。圖的資料結構為adjacency matrix的話,便是O(V^2 + WV);圖的資料結構為adjacency lists的話,便是O(V+E + WV)。

Single Source Shortest Paths:
Label Correcting Algorithm
(Bellman-Ford Algorithm)

程度★ 難度★★★

註記

http://www.walden-family.com/public/bf-history.pdf

此演算法誤植情況相當嚴重,包括CLRS、維基百科,記載的並非原始版本。

此演算法亦經由西南交通大学段凡丁《关于最短路径的SPFA快速算法》重新發現,於是中文網路出現了Shortest Path Faster Algorithm, SPFA的通俗稱呼,後來更有中文書籍不明就裡胡亂引用。學術上根本查無此稱呼。

此演算法的貢獻者除了Bellman與Ford以外,其實還有另外一人Moore。按照論文發表的年代順序,Ford首先發現Label Correcting的技巧,但是沒有特別規定計算順序;Moore發現由起點開始,不斷朝鄰點擴展,是一個不錯的計算順序(等同使用queue);Bellman發現此演算法可套用Dynamic Programming的思路,並證明每個點最多重新標記V-1次,演算法就可以結束。

約五年後,Ford與Fulkerson改良此演算法,成為分散式演算法,後人稱作Distance-vector Routing,是知名的網路路由協定──即是CLRS、維基百科記載的版本。

用途

一張有向圖,選定一個起點,求出起點到圖上各點的最短路徑,即是最短路徑樹。可以順便偵測圖上是否有負環,但是無法找出負環所在位置。

圖上有負邊,就無法使用Label Setting Algorithm。不在樹上、離根最近的點,受負邊影響,不見得是最短路徑。

圖上有負邊,則可以使用Label Correcting Algorithm。就算數值標記錯了,仍可修正。

想法:求出最短路徑樹

先前介紹relaxation說到:不斷尋找捷徑以縮短原本路徑,終會得到最短路徑。

一條捷徑如果很長,就不好辦了。一條捷徑如果很長,可以拆解成一條一條的邊,並一一嘗試以這些邊作為捷徑。只要不斷重複嘗試,逐步更新最短路徑長度,一條一條的邊終會連接成一條完整的捷徑。這是Greedy Method的概念。

從relaxation的角度來看:

Label Setting Algorithm只有正邊、零邊,知道relaxation的正確順序,逐步設定每個點的最短路徑長度。

Label Correcting Algorithm受負邊影響,不知道relaxation的正確順序,只好不斷尋找捷徑、不斷校正每個點的最短路徑長度,直到正確為止。

想法:偵測負環

如果一張圖上面有負環,那麼建立一條經過負環的捷徑,便會讓路徑縮短一些;只要不斷地建立經過負環的捷徑,不斷地繞行負環,那麼路徑就會無限縮短下去,成為無限短。

一條最短路徑最多只有V-1條邊。當一個點被標記超過V-1次,表示其最短路徑超過V-1條邊(讀者請自行推敲),必定經過負環!

演算法

令w[a][b]是a點到b點的距離(即是邊的權重)。
令d[a]是起點到a點的最短路徑長度,起點設為零,其他點都設為無限大。

一、重複下面這件事,直到圖上每一條邊都無法作為捷徑:
 甲、找到一條可以作為捷徑的邊ab:d[a] + w[a][b] < d[b]。
 乙、以邊ab來修正起點到b點的最短路徑:d[b] = d[a] + w[a][b]。
 丙、如果b點被標記V次以上,表示圖上有負環。演算法立刻結束。
找出最短路徑樹+偵測負環
  1. int w[9][9];    // 一張有權重的圖:adjacency matrix
  2. int d[9];       // 紀錄起點到各個點的最短路徑長度
  3. int parent[9];  // 紀錄各個點在最短路徑樹上的父親是誰
  4. int n[9];       // 記錄各個點被標記幾次,初始化為零。
  5. void label_correcting(int source)
  6. {
  7.     n[0...9-1] = 0;             // 初始化
  8.     d[source] = 0;              // 設定起點的最短路徑長度
  9.     parent[source] = source;    // 設定起點是樹根(父親為自己)
  10.     n[source]++;                // 起點被標記了一次
  11.     while (還能找到一條邊abd[a] + w[a][b] < d[b])
  12.     {
  13.         d[b] = d[a] + w[a][b];  // 更新由起點到b點的最短路徑長度
  14.         parent[b] = a;          // b點是由a點延伸過去的
  15.         if (++n[b] >= 9return;// 圖上有負環,最短路徑樹不存在!
  16.     }
  17. }

演算法

想要亂槍打鳥、直接找到可以做為捷徑的邊,並不是那麼容易的。腳踏實地的方式是:一個點一旦被重新標記,就讓該點所有出邊嘗試做為捷徑,更新鄰點的最短路徑長度。

令w[a][b]是a點到b點的距離(即是邊的權重)。
令d[a]是起點到a點的最短路徑長度,起點設為零,其他點都設為無限大。
LIST是一個存放點的容器,可以是stack、queue、set、……。

一、把起點放入LIST。
二、重複下面這件事,直到LIST沒有東西為止:
 甲、從LIST中取出一點,作為a點。
 乙、找到一條可以作為捷徑的邊ab:d[a] + w[a][b] < d[b]。
 丙、以邊ab來修正起點到b點的最短路徑:d[b] = d[a] + w[a][b]。
 丁、將b點加到LIST當中。
 戊、如果b點被標記V次以上,表示圖上有負環。演算法立刻結束。
找出最短路徑樹+偵測負環(adjacency matrix)
  1. int w[9][9];    // 一張有權重的圖:adjacency matrix
  2. int d[9];       // 紀錄起點到各個點的最短路徑長度
  3. int parent[9];  // 紀錄各個點在最短路徑樹上的父親是誰
  4. int n[9];       // 記錄各個點被標記幾次,初始化為零。
  5. void label_correcting(int source)
  6. {
  7.     memset(n0sizeof(n));    // 初始化
  8.     d[source] = 0;              // 設定起點的最短路徑長度
  9.     parent[source] = source;    // 設定起點是樹根(父親為自己)
  10.     n[source]++;                // 起點被標記了一次
  11.     queue<intQ;               // 一個存放點的容器:queue
  12.     Q.push(source);             // 將起點放入容器當中
  13.     
  14.     while (!Q.empty())
  15.     {
  16.         int a = Q.front(); Q.pop();     // 從容器中取出一點,作為a點
  17.         for (int b=0b<9 ++b)
  18.             if (d[a] + w[a][b] < d[b])
  19.             {
  20.                 if (++n[b] >= 9return;// 圖上有負環,最短路徑樹不存在
  21.                 d[b] = d[a] + w[a][b];  // 修正起點到b點的最短路徑長度
  22.                 parent[b] = a;          // b點是由a點延伸過去的
  23.                 Q.push(b);              // 將b點放入容器當中
  24.             }
  25.     }
  26. }

容器亦可改用Priority Queue,自行訂立一套優先順序,以加速演算法。Small Label First(SLF)、Large Label Last(LLL)都是不錯的選擇。

時間複雜度

圖的資料結構為adjacency list的話:

每當重新標記一個點,就讓該點所有出邊嘗試做為捷徑。每個點最多標記V次。

每個點都標記一次時,需時O(V+E)。每個點都標記V次時,需時O(V*(V+E)),可簡單寫成O(VE)。因此總時間複雜度為O(VE)。

實務上,每個點僅標記一至二次,平均時間複雜度為O(V+E)。效率極高。

圖的資料結構為adjacency matrix的話,時間複雜度為O(V^3)。平均時間複雜度為O(V^2)。

UVa 10557 10682

Single Source Shortest Paths:
Distance-vector Routing
(Distributed Bellman-Ford Algorithm)

程度★ 難度★★

演算法:找出最短路徑樹

圖上每一條邊依序(或同時)當作捷徑,進行relaxation。重覆V-1次。

【待補圖片】

令w[a][b]是a點到b點的距離(即是邊的權重)。
令d[a]是起點到a點的最短路徑長度,起點設為零,其他點都設為無限大。

一、重複下面這件事V-1次:
 甲、窮舉所有邊ab,
 乙、找到所有可以作為捷徑的邊ab:d[a] + w[a][b] < d[b]。
 丙、以邊ab來修正起點到b點的最短路徑:d[b] = d[a] + w[a][b]。
找出最短路徑樹(adjacency matrix)
  1. int w[9][9];    // 一張有權重的圖
  2. int d[9];       // 紀錄起點到各個點的最短路徑長度
  3. int parent[9];  // 紀錄各個點在最短路徑樹上的父親是誰
  4. void distance_vector(int source)
  5. {
  6.     for (int i=0i<9i++) d[i] = 1e9// initialize
  7.     d[source] = 0;              // 設定起點的最短路徑長度
  8.     parent[source] = source;    // 設定起點是樹根(父親為自己)
  9.     for (int i=0i<9-1i++)   // 重覆步驟V-1次
  10.         for (int a=0a<9; ++a// 全部的邊都當作捷徑
  11.             for (int b=0b<9; ++b)
  12.                 if (d[a] + w[a][b] < d[b])
  13.                 {
  14.                     d[b] = d[a] + w[a][b];
  15.                     parent[b] = a;
  16.                 }
  17. }
找出最短路徑樹(adjacency lists)
  1. struct Element {int vw;} w[9];    // 一張有權重的圖
  2. int size[9];
  3. int d[9];
  4. int parent[9];
  5. void distance_vector(int source)
  6. {
  7.     for (int i=0i<9i++) d[i] = 1e9;
  8.     d[source] = 0;
  9.     parent[source] = source;
  10.     for (int i=0i<9-1i++)
  11.         for (int a=0a<9a++)
  12.             for (int j=0j<size[a]; j++)
  13.             {
  14.                 int b = w[a][j].bw = w[a][j].w;
  15.                 if (d[a] + w < d[b])
  16.                 {
  17.                     d[b] = d[a] + w;
  18.                     parent[b] = a;
  19.                 }
  20.             }
  21. }
找出最短路徑樹(edge list)
  1. struct Edge {int abw;}; // 紀錄一條邊的資訊
  2. Edge w[13]; // 將所有邊依序放進陣列之中,成為一張有權重的圖
  3. int d[9];
  4. int parent[9];
  5. void distance_vector(int source)
  6. {
  7.     for (int i=0i<9i++) d[i] = 1e9;
  8.     d[source] = 0;
  9.     parent[source] = source;
  10.     for (int i=0i<9-1i++)
  11.         for (int j=0j<13j++)
  12.         {
  13.             int a = w[j].ab = w[j].bw = w[j].w;
  14.             if  (d[a] + w < d[b])
  15.             {
  16.                 d[b] = d[a] + w;
  17.                 parent[b] = a;
  18.             }
  19.         }
  20. }

時間複雜度:圖的資料結構為adjacency matrix的話,便是O(V^3);圖的資料結構為adjacency lists的話,便是O(VE)。

實際效率不如Label Correcting Algorithm。

演算法:偵測負環

每個點已經標記了V-1次。若還能找到捷徑,使某個點變成標記V次,則有負環。

只需一次Graph Traversal的時間,便能偵測負環。

偵測負環(adjacency matrix)
  1. bool negative_cycle()
  2. {
  3.     for (int a=0a<9; ++a)
  4.         for (int b=0b<9; ++b)
  5.             if (d[a] + w[a][b] < d[b])
  6.                 return true;
  7.     return false;
  8. }

UVa 558

Single Source Shortest Paths:
Scaling

程度★★ 難度★★★

用途

一張有向圖,選定一個起點,求出起點到圖上各點的最短路徑,即是最短路徑樹。但是限制是:圖上每一條邊的權重皆非負數。但是限制是:圖上每一條邊的權重皆非負整數。

演算法(Gabow's Algorithm)

詳細內容可參考CLRS習題24-4,此處僅略述。

重複以下步驟O(logC)次,每個步驟要求出當下的最短路徑:
1. 令權重更加精細。
2. 以上一步驟算得的最短路徑長度來調整權重。
   並以調整後的權重求最短路徑,可用O(V+E)時間求得。
   (調整過的權重剛好皆為非負數,且最短路徑長度都不會超過E。)
3. 還原成正確的最短路徑長度。

Scaling的精髓,在於每次增加精細度後,必須有效率的修正前次與今次的誤差。此演算法巧妙運用調整權重的技術,確切找出前次與今次差異之處,而得以用O(E)時間修正誤差。

上述O(V+E)求最短路徑的演算法,仍是運用Dijkstra's Algorithm「最近的點先找」概念,只是求法有點小改變。首先開個E+1條linked list,離起點距離為x的點,就放在第x條。只要依序掃描一遍所有的linked list,就可以求出最短路徑了。

 
  1. const int V = 9E = 9 * 8 / 2;
  2. int w[9][9];    // 一張有權重的圖(adjacency matrix)
  3. int d[9];       // 紀錄起點到各個點的最短路徑長度  
  4. void shortest_path(int s)
  5. {
  6.     vector<intlist[E+1];
  7.     list[0].push_back(s);
  8.     for (int i=0i<=E; ++i)
  9.         for (int j=0j<list[i].size(); ++j)
  10.         {
  11.             int u = list[i][j];
  12.             if (d[uis not filled)
  13.             {
  14.                 d[u] = i;
  15.                 // relaxation
  16.                 for (int v=0v<V; ++v)
  17.                     if (d[vis not filled && i + w[u][v] <= E)
  18.                         list[i + w[u][v]].push_back(v);
  19.             }
  20.         }
  21. }

時間複雜度

整個演算法共有O(logC)個步驟,C是整張圖權重最大的邊的權重。

圖的資料結構為adjacency matrix的話,每一步驟需要O(V^2)時間,整體時間複雜度為O(V^2 * logC);圖的資料結構為adjacency lists的話,每一步驟需要O(V+E)時間(簡單記為O(E)),整體時間複雜度為O(ElogC)。

計算最短路徑的長度(adjacency lists)

【待補程式碼】

找出最短路徑樹(adjacency lists)

【待補程式碼】

Single Source Shortest Paths in DAG:
Topological Sort

程度★ 難度★★★

用途

一張有向無環圖(Directed Acyclic Graph, DAG),選定一個起點,求出起點到圖上各點的最短路徑,即是最短路徑樹。

演算法

此演算法為Topological Sort加上Dynamic Programming,與Activity on Edge Network的演算法如出一轍,可參考本站文件「Topological Sort」。

一張圖經過Topological Sort之後,便可以確定圖上每一個點只會往排在後方的點走去(由排在前方的點走過來)。計算順序相當明確,因此可以利用Dynamic Programming來計算各條最短路徑。

1. 進行Topological Sort。
2. 依照拓樸順序(或者逆序),對各點進行relaxation。

這個演算法可以看做是,每次都知道最小值在哪一點的Dijkstra's Algorithm。

時間複雜度

時間複雜度約是兩次Graph Traversal的時間複雜度。圖的資料結構為adjacency matrix的話,便是O(V^2);圖的資料結構為adjacency lists的話,便是O(V+E)。

找出最短路徑樹(adjacency matrix)

 
  1. bool w[9][9];   // adjacency matrix
  2. int topo[9];    // 經過拓樸排序後的順序
  3. int parent[9];  // 紀錄各個點在最短路徑樹上的父親是誰
  4. void shortest_path_tree(int source)
  5. {
  6.     for (int i=0i<9i++) visit[i] = false;
  7.     for (int i=0i<9i++) d[i] = 1e9;
  8.     // 找出起點是在拓樸排序中的哪一個位置
  9.     int p = 0;
  10.     while (p < 9 && topo[p] != sourcep++;
  11.     // 計算最短路徑長度
  12.     d[p] = 0;       // 設定起點的最短路徑長度
  13.     parent[p] = p;  // 設定起點是樹根(父親為自己)
  14.     for (int i=pi<9; ++i// 看看每一個點可連向哪些點
  15.         for (int j=i+1j<9; ++j)
  16.         {
  17.             int a = topo[i], b = topo[j];
  18.             if (d[a] + w[a][b] < d[b])
  19.             {
  20.                 d[b] = d[a] + w[a][b];
  21.                 parent[b] = a;
  22.             }
  23.         }
  24. }

迴圈的部分還有另一種寫法。

 
  1.     for (int j=p+1j<9; ++j)   // 看看每一個點被哪些點連向
  2.         for (int i=0i<j; ++i)
  3.         {
  4.             int a = topo[i], b = topo[j];
  5.             if (d[a] + w[a][b] < d[b])
  6.             {
  7.                 d[b] = d[a] + w[a][b];
  8.                 parent[b] = a;
  9.             }
  10.         }
posted @ 2013-04-25 12:44  清灵阁主  阅读(511)  评论(0编辑  收藏  举报