單源最短路徑(1):Dijkstra算法

原文: 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→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步:全部點都已找到,中止。編碼

對於上述步驟,你可能存在如下的疑問: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)$的時間;
  • 由於是最小堆,因此每次取出最小值只需$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[]也是錯的。

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

相關文章
相關標籤/搜索