• 路径算法


    转自: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.         }
  • 相关阅读:
    谷歌插件Web Scraper爬虫
    jstack的使用
    SPOTLIGHT的安装和使用
    jmeter时间处理
    Java监控和管理控制台
    【学习笔记】分析性能问题的一般步骤
    混合场景——Jmeter之吞吐量控制器
    瞬间并发测试jmeter
    关于WebView2,是否可以入坑,及踩坑
    jvm调优
  • 原文地址:https://www.cnblogs.com/LCGIS/p/3042240.html
Copyright © 2020-2023  润新知