1. 程式人生 > >單源最短路徑——迪傑斯特拉(Dijkstra)演算法 C++實現

單源最短路徑——迪傑斯特拉(Dijkstra)演算法 C++實現

求最短路徑之Dijkstra演算法

Dijkstra演算法是用來求單源最短路徑問題,即給定圖G和起點s,通過演算法得到s到達其他每個頂點的最短距離。

基本思想:對圖G(V,E)設定集合S,存放已被訪問的頂點,然後每次從集合V-S中選擇與起點s的最短距離最小的一個頂點(記為u),訪問並加入集合S。之後,令u為中介點,優化起點s與所有從u能夠到達的頂點v之間的最短距離。這樣的操作執行n次(n為頂點個數),直到集合S已經包含所有頂點。
 

由於圖可以使用鄰接矩陣或者鄰接表來實現,因此會有兩種寫法。以下圖為例來具體實現程式碼:

 

程式碼:

main.cpp

#include <iostream>
#include <vector>
using namespace std;

const int INF = 1e9; // int範圍約為 (-2.15e9, 2.15e9)

/*Dijkstra演算法解決的是單源最短路徑問題,即給定圖G(V,E)和起點s(起點又稱為源點),邊的權值為非負,
求從起點s到達其它頂點的最短距離,並將最短距離儲存在矩陣d中*/
void Dijkstra(int n, int s, vector<vector<int>> G, vector<bool> &vis, vector<int> &d, vector<int> &pre)
{
    /*
     *   n:        頂點個數
     *   s:        源點
     *   G:        圖的鄰接矩陣
     * vis:        標記頂點是否已被訪問
     *   d:        儲存源點s到達其它頂點的最短距離
     * pre:        最短路徑中v的前驅結點
     */

    // 初始化
    fill(vis.begin(), vis.end(), false);
    fill(d.begin(), d.end(), INF);
    d[s] = 0;
    for (int i = 0; i < n; ++i)
    {
        pre[i] = i;
    }

    // n次迴圈,確定d[n]陣列
    for (int i = 0; i < n; ++i)
    {
        // 找到距離s最近的點u,和最短距離d[u]
        int u = -1;
        int MIN = INF;
        for (int j = 0; j < n; ++j)
        {
            if (!vis[j] && d[j] < MIN)
            {
                u = j;
                MIN = d[j];
            }
        }

        // 找不到小於INF的d[u],說明剩下的頂點與起點s不連通
        if (u == -1)
        {
            return;
        }

        vis[u] = true;
        for (int v = 0; v < n; ++v)
        {
            // 遍歷所有頂點,如果v未被訪問 && 可以達到v && 以u為中介點使d[v]更小
            if (!vis[v] && G[u][v] != INF && d[u] + G[u][v] < d[v])
            {
                d[v] = d[u] + G[u][v];   // 更新d[v]
                pre[v] = u;              // 記錄v的前驅頂點為u(新新增)
            }
        }
    }
}

// 輸出從起點s到頂點v的最短路徑
void DFSPrint(int s, int v, vector<int> pre)
{
    if (v == s)
    {
        cout << s << " ";
        return;
    }
    DFSPrint(s, pre[v], pre);
    cout << v << " ";
}


int main()
{
    int n = 6;
    /*鄰接矩陣*/
    vector<vector<int>> G = {{  0,  4,INF,INF,  1,  2},
                             {  4,  0,  6,INF,INF,  3},
                             {INF,  6,  0,  6,INF,  5},
                             {INF,INF,  6,  0,  4,  5},
                             {  1,INF,INF,  4,  0,  3},
                             {  2,  3,  5,  5,  3,  0}};
    vector<bool> vis(n);
    vector<int> d(n);
    vector<int> pre(n);

    Dijkstra(n, 0, G, vis, d, pre);

    for (size_t i = 0; i < d.size(); ++i)
    {
        cout << "the shortest path " << i << " is: " << d[i] << endl;
    }
    cout << endl;

    // v = 2: 0->5->2  cost = 2 + 5 = 7
    // v = 3: 0->4->3  cost = 1 + 4 = 5
    int v = 2;
    DFSPrint(0, v, pre);
    cout << endl << "cost = " << d[v] << endl;

    return 0;
}

執行結果:

 

複雜度分析:

主要是外層的迴圈O(V)(V就是頂點個數n)與內層迴圈(尋找最小的d[u]需要O(V)、列舉需要O(V)產生的),總的時間複雜度為O(V*(V+V))=O(V^2)

 

Dijkstra演算法與Prim演算法的聯絡:

前者每次尋找與最近的結點

後者每次尋找與最近的結點

 

總結:

Dijkstra演算法只能應對所有邊權都是非負數的情況,如果邊權出現負數,那麼Dijkstra演算法很可能會出錯,這是最好使用SPFA演算法。

上面的做法複雜度為O(V^2)級別,其中由於必須把每個頂點都標記已訪問,因此外層迴圈的O(V)時間是無法避免的,但是尋找最小d[u]的過程

卻可以不必達到O(V)的複雜度,而可以使用對優化來降低複雜度。最簡單的寫法是直接使用STL中的優先佇列priority_queue,這樣使用鄰接表實現Dijkstra演算法的時間複雜度可以降低為O(VlogV+E)。


如果題目給出的是無向邊(即雙向邊)而不是有向邊,又該如何解決呢?其實很簡單,只需要把無向邊當成兩條指向相反的有向邊即可。對鄰接矩陣來說,一條u與v之間的無向邊在輸入時可以分別對G[u][v]和G[v][u]賦以相同的邊權;而對於鄰接表來說,只需要在u的鄰接表Adj[u]末尾新增上v,並在v的鄰接表Adj[v]末尾新增上u即可。
 

(4)、Dijkstra演算法求解實際問題
之前講的是最基本的Dijkstra演算法,那麼平時考試筆試等遇到的題目肯定不會這麼“裸”,更多時候會出現這樣一種情況,即從起點到終點的最短距離最小的路徑不止一條。
那麼碰到這種兩條以上可以達到最短距離的路徑,題目就會給出一個第二標尺(第一標尺是距離),要求在所有最短路徑中選擇第二標尺最優的一條路徑,而第二標尺常見的是以下三種出題方法或者其組合:
給每條邊在增加一個邊權(比如說花費),然後要求在最短路徑有多條時要求路徑上的花費之和最小(當然如果邊權是其它含義,也可以是最大)
給每個點增加一個點權(例如每個城市能收集到的物資),然後在最短路徑有多條時要求路徑上的點權之和最大(當然如果是其它含義,也可以是最小)
直接問有多少條最短路徑

解決思路:都只需要增加一個數組來存放新增的邊權或點權或最短路徑條數,然後在Dijkstra演算法中修改優化d[v]的那個步驟即可,其它部分不需要改動。
如下:
新增邊權。以新增的邊權代表花費為例,用cost[u][v]表示u->v的花費(由題目輸入),並增加一個數組c[],令從起點s到達頂點u的最少花費為c[u],初始化時只有c[s]=0,其餘均為INF(一個很大的值),這樣就可以在更新d[v]時更新c[v]. 程式碼如下:
 


for(int v=0; v<n; v++)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               c[v] = c[u] + cost[u][v]; 
          }
          else if(d[u]+G[u][v] == d[v] && c[u]+cost[u][v] < c[v])
               c[v]=c[u]+cost[u][v];          //最短距離相等時看能都使c[v]更優
     }

 

新增點權。以新增的點權代表城市中能收集到的物資為例,用weight[u]表示城市u中的物資數目(由題目輸入),並增加一個數組w[],令起點s到達頂點u可以收集到的最大物資為w[u],初始化時只有w[s]為weight[s],其餘均為0,這樣就可以在更新d[v]時更新w[v].程式碼如下:
 


for(int v=0; v<n; ++v)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               w[v] = w[u]+weight[v]; 
          }
          else if(d[u]+G[u][v] == d[v] && w[u]+weight[v]>w[v])
               w[v] = w[u]+weight[v];
     }
}

求最短路徑條數。只需要新增一個數組num[],令從起點s到達頂點u的最短路徑條數為num[u],初始化時只有num[s]=1,其餘均為0,這樣就可以在更新d[v]時讓num[v]=num[u],而當d[u]+G[u][v] =d[v]時,讓num[v]+=num[u].程式碼如下:


for(int v=0; v<n; ++v)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               num[v]=num[u];
          }
          else if(d[u]+G[u][v] == d[v])
               num[v]+=num[u];
     }
}

若需要將多條最短路徑打印出來,則需要將記錄前驅結點的陣列int pre[n]改為二維陣列vector<vector<int>> pre(n, vector<int>());

並在查詢到相同路徑時,採用push_back()同時儲存多個前驅結點,而在找到更短路徑時,需要clear()清空之前所保持的前驅結點,並再儲存當前最短路徑下的前驅結點,在列印路徑時同樣採用DFS即可,儲存路徑部分程式碼如下:


for(int v=0; v<n; ++v)
{
     if(vis[v]==false && G[u][v]!=INF)
     {
          if(d[u]+G[u][v] < d[v])
          {
               d[v] = d[u]+G[u][v];
               num[v]=num[u];
        
               // 清空前驅結點,並只保留當前這一個前驅
               pre[v].clear();
               pre[v].push_back(u);
          }
          else if(d[u]+G[u][v] == d[v])
               num[v]+=num[u];
        
               // 同時保留多個前驅結點
               pre[v].push_back(u);
     }
}

參考資料:

https://blog.csdn.net/YF_Li123/article/details/74090301

普林斯頓演算法公開課:Algorithms - Robert Sedgewick, Kevin Wayne