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)加入到最短路徑樹。
實作
- int w[9][9]; // 一張有權重的圖:adjacency matrix
- int d[9]; // 紀錄起點到圖上各個點的最短路徑長度
- int parent[9]; // 紀錄各個點在最短路徑樹上的父親是誰
- bool visit[9]; // 紀錄各個點是不是已在最短路徑樹之中
- void label_setting(int source)
- {
- for (int i=0; i<100; i++) visit[i] = false; // initialize
- d[source] = 0; // 設定起點的最短路徑長度
- parent[source] = source; // 設定起點是樹根(父親為自己)
- visit[source] = true; // 將起點加入到最短路徑樹
- for (int k=0; k<9-1; k++) // 將剩餘所有點加入到最短路徑樹
- {
- // 從既有的最短路徑樹,找出一條聯外而且是最短的邊
- int a = -1, b = -1, min = 1e9;
- // 找一個已在最短路徑樹上的點
- for (int i=0; i<9; i++)
- if (visit[i])
- // 找一個不在最短路徑樹上的點
- for (int j=0; j<9; j++)
- if (!visit[j])
- if (d[i] + w[i][j] < min)
- {
- a = i; // 記錄這一條邊
- b = j;
- min = d[i] + w[i][j];
- }
- // 起點有連通的最短路徑都已找完
- if (a == -1 || b == -1) break;
- // // 不連通即是最短路徑長度無限長
- // if (min == 1e9) break;
- d[b] = min; // 儲存由起點到b點的最短路徑長度
- parent[b] = a; // b點是由a點延伸過去的
- visit[b] = true; // 把b點加入到最短路徑樹之中
- }
- }
換個角度看事情
前面有提到 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) 。
實作
- int w[9][9]; // 一張有權重的圖
- int d[9]; // 紀錄起點到各個點的最短路徑長度
- int parent[9]; // 紀錄各個點在最短路徑樹上的父親是誰
- bool visit[9]; // 紀錄各個點是不是已在最短路徑樹之中
- void dijkstra(int source)
- {
- for (int i=0; i<9; i++) visit[i] = false; // initialize
- for (int i=0; i<9; i++) d[i] = 1e9;
- d[source] = 0;
- parent[source] = source;
- for (int k=0; k<9; k++)
- {
- int a = -1, b = -1, min = 1e9;
- for (int i=0; i<9; i++)
- if (!visit[i] && d[i] < min)
- {
- a = i; // 記錄這一條邊
- min = d[i];
- }
- if (a == -1) break; // 起點有連通的最短路徑都已找完
- // if (min == 1e9) break; // 不連通即是最短路徑長度無限長
- visit[a] = true;
- for (b=0; b<9; b++) // 把起點到b點的最短路徑當作捷徑
- if (!visit[b] && d[a] + w[a][b] < d[b])
- {
- d[b] = d[a] + w[a][b];
- parent[b] = a;
- }
- }
- }