原文: https://subetter.com/algorith...html
Dijkstra算法(中文名:迪傑斯特拉算法)是由荷蘭計算機科學家Edsger Wybe Dijkstra提出。該算法經常使用於路由算法或者做爲其餘圖算法的一個子模塊。舉例來講,若是圖中的頂點表示城市,而邊上的權重表示城市間開車行經的距離,該算法能夠用來找到兩個城市之間的最短路徑。ios
咱們用一個例子來具體說明迪傑斯特拉算法的流程。c++
定義源點爲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→3
比dist[3]
大,故不更新dist[3]
;2與4鄰接,因0→2→4
比dist[4]
小,故更新dist[4]
爲4。在dist[]
中找到最小值,其頂點爲3,即此時又找到0到3的最短路。數組
第3步:從3開始,繼續更新dist[]
數組:3與1鄰接,因0→3→1
比dist[1]
小,更新dist[1]
爲5;3與4鄰接,因0→3→4
比dist[4]
大,故不更新。在dist[]
中找到最小值,其頂點爲4,即此時又找到0到4的最短路。性能
第4步:從4開始,繼續更新dist[]
數組:4與1不鄰接,不更新。在dist[]
中找到最小值,其頂點爲1,即此時又找到0到1的最短路。優化
第5步:全部點都已找到,中止。編碼
對於上述步驟,你可能存在如下的疑問:spa
若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次。
#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 edge_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 >> edge_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 < edge_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; }
輸入數據,結果爲:
請輸入圖的頂點數(<100):5 請輸入圖的邊數:7 請輸入邊的信息: 0 1 3 0 2 1 0 3 2 1 3 3 2 3 2 3 4 3 2 4 3 請輸入源點(<5):0 0 到 1 的最短距離是:3,路徑是:1--0 0 到 2 的最短距離是:1,路徑是:2--0 0 到 3 的最短距離是:2,路徑是:3--0 0 到 4 的最短距離是:4,路徑是:4--2--0
設圖的邊數爲m,頂點數爲n。
Dijkstra算法最簡單的實現方法是用一個數組來存儲全部頂點的dist[]
(即本程序採用的策略),因此搜索dist[]
中最小元素的運算須要線性搜索$O(n)$。這樣的話算法的運行時間是$O(n^2)$。
對於邊數遠少於$n^2$的稀疏圖來講,咱們能夠用鄰接表來更有效的實現該算法,同時須要將一個二叉堆或者斐波納契堆用做優先隊列來查找最小的頂點。當用到二叉堆的時候,算法所需的時間爲 $O((m+n)logn)$,斐波納契堆能稍微提升一些性能,讓算法運行時間達到$O(m+nlogn)$。然而,使用斐波納契堆進行編程,經常會因爲算法常數過大而致使速度沒有顯著提升。
關於$O((m+n)logn)$的由來,我簡單的證實了下:
dist[]
數組調整成最小堆,須要$O(n)$的時間;dist[]
的改變,所以還須要$O(logn)$的時間來進行最小堆的從新調整(從當前dist[]
改變的位置往上調整)。綜上所述:總的時間複雜度爲:$O(n)+O(nlogn)+O(mlogn)=O((m+n)logn)$
最後簡單說下Dijkstra優化時二叉堆的兩種實現方式:
dist[]
壓在一個結構體再放進隊列裏;heap[]
,存儲頂點序號,再用一個數組pos[]
記錄第i個頂點在堆中的位置。相比之下,前者的編碼難度較低,所以在平時編程甚至算法競賽中,都是首選。
Dijkstra算法有個巨大的缺陷,請考慮下面這幅圖:
u→v
間存在一條負權迴路(所謂的負權迴路,維基和百科並未收錄其名詞,但從網上的一致態度來看,其含義爲:若是存在一個環(從某個點出發又回到本身的路徑),並且這個環上全部權值之和是負數,那這就是一個負權環,也叫負權迴路),那麼只要無限次地走這條負權迴路,即可以無限制地減小它的最短路徑權值,這就變相地說明最短路徑不存在。一個不存在最短路徑的圖,Dijkstra算法沒法檢測出這個問題,其最後求解的dist[]
也是錯的。
那麼對於上述的「一個不存在最短路徑的圖」,咱們該用什麼方法來解決呢?請接着看本系列第二篇文章。