系列文章目錄

單源最短路徑(1):Dijkstra 算法
單源最短路徑(2):Bellman_Ford 算法
單源最短路徑(3):SPFA 算法
單源最短路徑(4):總結

一:背景展開目錄

Dijkstra 算法(中文名:迪傑斯特拉算法)是由荷蘭計算機科學家 Edsger Wybe Dijkstra 提出。該算法常用於路由算法或者作為其他圖算法的一個子模塊。舉例來說,如果圖中的頂點表示城市,而邊上的權重表示城市間開車行經的距離,該算法可以用來找到兩個城市之間的最短路徑。

 

二:算法過程展開目錄

我們用一個例子來具體說明迪傑斯特拉算法的流程。

 

定義源點為 0,dist[i]為源點 0 到頂點 i 的最短路徑。其過程描述如下:

步驟 dist[1] dist[2] dist[3] dist[4] 已找到的集合
第 1 步 8 1 2 +∞ {2}
第 2 步 8 × 2 4 {2, 3}
第 3 步 5 × × 4 {2, 3, 4}
第 4 步 5 × × × {2, 3, 4, 1}
第 5 步 × × × × {2, 3, 4, 1}

第 1 步:從源點 0 開始,找到與其鄰接的點:1,2,3,更新dist[]數組,因 0 不與 4 鄰接,故dist[4]為正無窮。在dist[]中找到最小值,其頂點為 2,即此時已找到 0 到 2 的最短路。

第 2 步:從 2 開始,繼續更新dist[]數組:2 與 1 不鄰接,不更新;2 與 3 鄰接,因0→2→3dist[3]大,故不更新dist[3] ;2 與 4 鄰接,因0→2→4dist[4]小,故更新dist[4]為 4。在dist[]中找到最小值,其頂點為 3,即此時又找到 0 到 3 的最短路。

第 3 步:從 3 開始,繼續更新dist[]數組:3 與 1 鄰接,因0→3→1dist[1]小,更新dist[1]為 5;3 與 4 鄰接,因0→3→4dist[4]大,故不更新。在dist[]中找到最小值,其頂點為 4,即此時又找到 0 到 4 的最短路。

第 4 步:從 4 開始,繼續更新dist[]數組:4 與 1 不鄰接,不更新。在dist[]中找到最小值,其頂點為 1,即此時又找到 0 到 1 的最短路。

第 5 步:所有點都已找到,停止。

對於上述步驟,你可能存在以下的疑問:

 

 

若 A 作為源點,與其鄰接的只有 B,C,D 三點,其dist[]最小時頂點為 C,即就可以確定A→C為 A 到 C 的最短路。但是我們存在疑問的是:是否還存在另一條路徑使 A 到 C 的距離更小? 用反證法證明。

假設存在如上圖的紅色虛線路徑,使A→D→C的距離更小,那么A→D作為A→D→C的子路徑,其距離也比A→C小,這與前面所述 “dist[]最小時頂點為 C” 矛盾,故假設不成立。因此這個疑問不存在。

根據上面的證明,我們可以推斷出,Dijkstra 每次循環都可以確定一個頂點的最短路徑,故程序需要循環 n-1 次。

三:完整代碼展開目錄

/** * * author 劉毅(Limer) * date 2017-05-17 * mode C++ */ #include<iostream> using namespace std; int matrix[100][100]; //鄰接矩陣 bool visited[100]; //標記數組 int dist[100]; //源點到頂點i的最短距離 int path[100]; //記錄最短路的路徑 int source; //源點 int vertex_num; //頂點數 int arc_num; //弧數 void Dijkstra(int source) { memset(visited, 0, sizeof(visited)); //初始化標記數組 visited[source] = true; for (int i = 0; i < vertex_num; i++) { dist[i] = matrix[source][i]; path[i] = source; } int min_cost; //權值最小 int min_cost_index; //權值最小的下標 for (int i = 1; i < vertex_num; i++) //找到源點到另外vertex_num-1個點的最短路徑 { min_cost = INT_MAX; for (int j = 0; j < vertex_num; j++) { if (visited[j] == false && dist[j] < min_cost) //找到權值最小 { min_cost = dist[j]; min_cost_index = j; } } visited[min_cost_index] = true; //該點已找到,進行標記 for (int j = 0; j < vertex_num; j++) //更新dist數組 { if (visited[j] == false && matrix[min_cost_index][j] != INT_MAX && //確保兩點之間有弧 matrix[min_cost_index][j] + min_cost < dist[j]) { dist[j] = matrix[min_cost_index][j] + min_cost; path[j] = min_cost_index; } } } } int main() { cout << "請輸入圖的頂點數(<100):"; cin >> vertex_num; cout << "請輸入圖的弧數:"; cin >> arc_num; for (int i = 0; i < vertex_num; i++) for (int j = 0; j < vertex_num; j++) matrix[i][j] = (i != j) ? INT_MAX : 0; //初始化matrix數組 cout << "請輸入弧的信息:\n"; int u, v, w; for (int i = 0; i < arc_num; i++) { cin >> u >> v >> w; matrix[u][v] = matrix[v][u] = w; } cout << "請輸入源點(<" << vertex_num << "):"; cin >> source; Dijkstra(source); for (int i = 0; i < vertex_num; i++) { if (i != source) { cout << source << "到" << i << "最短距離是:" << dist[i] << ",路徑是:" << i; int t = path[i]; while (t != source) { cout << "--" << t; t = path[t]; } cout << "--" << source << endl; } } return 0; }

輸入數據,結果為:

 

 

四:時間復雜度展開目錄

設圖的邊數為 m,頂點數為 n。

Dijkstra 算法最簡單的實現方法是用一個數組來存儲所有頂點的dist[](即本程序采用的策略),所以搜索dist[]中最小元素的運算需要線性搜索 O(n)。這樣的話算法的運行時間是 O(n2)。

對於邊數遠少於 n2 的稀疏圖來說,我們可以用鄰接表來更有效的實現該算法。同時需要將一個二叉堆或者斐波納契堆用作優先隊列來查找最小的頂點。當用到二叉堆的時候,算法所需的時間為 O((m+n)logn),斐波納契堆能稍微提高一些性能,讓算法運行時間達到 O(m+nlogn)。然而,使用斐波納契堆進行編程,常常會由於算法常數過大而導致速度沒有顯著提高。

關於 O((m+n)logn) 的由來,我簡單的證明了下(僅個人看法,不保證其正確性):

  • dist[]數組調整成最小堆,需要 O(n) 的時間;

  • 因為是最小堆,所以每次取出最小值只需 O(1) 的時間,接着把數組尾的數放置堆頂,並花費 O(logn) 的時間重新調整成最小堆;

  • 我們需要 n-1 次操作才可以找出剩下的 n-1 個點,在這期間,大約需要訪問 m 次邊,每次訪問都可能造成dist[]的改變,因此還需要 O(logn) 的時間來進行最小堆的重新調整(從當前dist[]改變的位置往上調整)。

綜上所述:總的時間復雜度為:O(n)+O(nlogn)+O(mlogn)=O((m+n)logn)

最后簡單說下 Dijkstra 優化時二叉堆的兩種實現方式:

  • 優先隊列,把每個頂點的序號和其dist[]壓在一個結構體再放進隊列里;

  • 自己建一個小頂堆heap[],存儲頂點序號,再用一個數組pos[]記錄第 i 個頂點在堆中的位置。

相比之下,前者的編碼難度較低,因此在平時編程甚至算法競賽中,都是首選。

五:該算法的缺陷展開目錄

Dijkstra 算法有個巨大的缺陷,請考慮下面這幅圖:

 

 

u→v間存在一條負權回路(所謂的負權回路,維基和百科並未收錄其名詞,但從網上的一致態度來看,其含義為:如果存在一個環(從某個點出發又回到自己的路徑),而且這個環上所有權值之和是負數,那這就是一個負權環,也叫負權回路),那么只要無限次地走這條負權回路,便可以無限制地減少它的最短路徑權值,這就變相地說明最短路徑不存在。一個不存在最短路徑的圖,Dijkstra 算法無法檢測出這個問題,其最后求解的dist[]也是錯的。

那么對於上述的 “一個不存在最短路徑的圖”,我們該用什么方法來解決呢?請接着看本系列第二篇文章。