几种最短路径算法

Path

程度★ 難度★★

「圖」與「道路地圖」

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

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

Path

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

如果起點到終點是不相通的,那麼就不會存在起點到終點的路徑。

路徑也有權重。把路徑上所有邊的權重,都加總起來,就是路徑的權重(通常只加總邊的權重,而不考慮點的權重)。路徑的權重,可以想像成路徑的總長度。

Simple Path

一條路徑,如果沒有重複地經過同樣的點,則稱做「簡單路徑」。

【註:一般情況下,當我們說「路徑」時,可以是指「簡單路徑」──這是因為「重覆地經過同樣的點的路徑」比較少用,而「簡單路徑」四個字又不如「路徑」兩個字來的簡潔。因此很多專有名詞便省略了「簡單」這兩個字,而直接使用「路徑」,但實際上是指「簡單路徑」。】

Longest Path

程度★ 難度★★

Longest Path

「最長路徑」。在一張權重圖上,兩點之間權重最大的簡單路徑。已被證明是 NP-Complete 問題。

Shortest Path

程度★ 難度★★

Shortest Path

「最短路徑」,在一張權重圖上,兩點之間權重最小的路徑。最短路徑不見得是邊最少、點最少的路徑。

最短路徑也可能不存在。兩點之間不連通、不存在路徑的時候,也就不會有最短路徑了。

Relaxation

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

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

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

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

Negative Cycle

權重為負值的環。以下簡稱負環。

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

大部分的最短路徑演算法都可以偵測出圖上是否有負環,不過有些卻不行。

無限長與無限短

當起點和終點之間不存在路徑的時候,也就不會有最短路徑了。這種情況有時候會被解讀成:從起點永遠走不到終點,所以最短路徑無限長。

當圖上有負環可做為捷徑的時候,這種情況則是:最短路徑無限短。

最短路徑都是簡單路徑

除了負環以外,如果一條路徑重複的經過同一條邊、同一個點,一定會讓路徑長度變長。由此可知:沒有負環的情況下,最短路徑都是簡單路徑,決不會經過同樣的點兩次,也決不會經過同樣的邊兩次。

Shortest Path Tree

當一張圖沒有負環時,在圖上選定一個點做為起點,由此起點到圖上各點的最短路徑們,會延展成一棵樹,稱作「最短路徑樹」。由於最短路徑不見得只有一條,以特定一點做為起點的最短路徑樹也不見得只有一種。

最短路徑樹上的每一條最短路徑,都是由其它的最短路徑延伸拓展而得(除了由起點到起點這一條最短路徑以外)。也就是說,最短路徑樹上的每一條最短路徑,都是以其他的最短路徑做為捷徑。

當兩點之間有多條邊

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

當兩點之間沒有邊

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

當圖的資料結構為 adjacency matrix 時,任兩點之間都一定要有一個權重值。要找最短路徑,不相鄰的兩點必須設定權重無限大,而不能使用零,以免計算錯誤;要找最長路徑,則是要設定權重無限小。

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

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

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

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

最短路徑演算法的原理類型,有向圖

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

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

註: Label 是指在圖上的點(或邊)標記數值或符號。

最短路徑演算法的原理類型,無向圖

需精通「 Matching 」、「 Circuit 」、「 T-Join 」等進階概念,因此以下文章不討論!

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

最短路徑演算法的原理類型,混合圖

已被證明是 NP-Complete 問題。

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. }

換個角度看事情

前面有提到 relaxtion 的概念。以捷徑的觀點來看,當下已求得的每一條最短路徑,都會作為捷徑,縮短所有由起點到圖上各點的路徑。每個步驟中所得到的最短路徑,由於比它更短的最短路徑全都嘗試做為捷徑過了,所以能夠確保是最短路徑。

Label Setting Algorithm 亦可看做是一種 Graph Traversal ,但與 BFS DFS 不同的地方在於 Label Setting Algorithm 有考慮權重,遍歷順序是先拜訪離樹根最近的點和邊。

Single Source Shortest Paths:
Label Setting Algorithm + Memoization
Dijkstra's Algorithm

程度★ 難度★★★

想法

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

w[a][b] 的角度來看,整個過程重覆窮舉了許多邊。

運用 Memoization ,隨時紀錄已經窮舉過的邊,避免重複窮舉,節省時間。

每當將一個 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)

時間複雜度

分為兩個部分討論。

甲、加入點、窮舉邊:每個點只加入一次,每條邊只窮舉一次,剛好等同於一次 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.         for (b=0b<9b++)     // 把起點到b點的最短路徑當作捷徑
  24.             if (!visit[b] && d[a] + w[a][b] < d[b])
  25.             {
  26.                 d[b] = d[a] + w[a][b];
  27.                 parent[b] = a;
  28.             }
  29.     }
  30. }
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值