1. 程式人生 > >C++ 求最短路徑問題之Dijkstra演算法(一)

C++ 求最短路徑問題之Dijkstra演算法(一)

求最短路徑之Dijkstra演算法

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

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

Dijkstra演算法虛擬碼:

//G為圖;陣列d為源點到達各點的最短路徑長度,s為起點
Dijkstra(G, d[], s)
{
     初始化;
     for(迴圈n次)
     {
          u = 使d[u]最小的還未被訪問的頂點的標號;
          記u已被訪問;
          for(從u出發能到達的所有頂點v)
          {
               if(v未被訪問 && 以u為中介點使s到頂點v的最短距離d[v]更優)
               {
                    優化d[v];
               }
          }
     }
}

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


(1)鄰接矩陣版

const int INF = 1000000000;

/*Dijkstra演算法解決的是單源最短路徑問題,即給定圖G(V,E)和起點s(起點又稱為源點),
求從起點s到達其它頂點的最短距離,並將最短距離儲存在矩陣d中*/
void Dijkstra(int n, int s, vector<vector<int>> G, vector<bool>& vis, vector<int>& d)
{
       /*
       param
       n:           頂點個數
       s:           源點
       G:           圖的鄰接矩陣
       vis:         標記頂點是否已被訪問
       d:           儲存源點s到達其它頂點的最短距離
       */
       fill(d.begin(), d.end(), INF);                         //初始化最短距離矩陣,全部為INF
       d[s] = 0;                                              //起點s到達自身的距離為0
       for (int i = 0; i < n; ++i)
       {
              int u = -1;                                     //找到d[u]最小的u
              int MIN = INF;                                  //記錄最小的d[u]
              for (int j = 0; j < n; ++j)                     //開始尋找最小的d[u]
              {
                     if (vis[j] == false && d[j] < MIN)
                     {
                           u = j;
                           MIN = d[j];
                     }
              }
              //找不到小於INF的d[u],說明剩下的頂點和起點s不連通
              if (u == -1)
                     return;
              vis[u] = true;                                  //標記u已被訪問
              for (int v = 0; v < n; ++v)
              {
                     //遍歷所有頂點,如果v未被訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
                     if (vis[v] == false && d[u] + G[u][v] < d[v])
                           d[v] = d[u] + G[u][v];             //更新d[v]
              }
       }
}

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

(2)鄰接表版
const int INF = 1000000000;

struct Node
{
       int v;         //邊的目標頂點
       int dis;       //dis為邊權
       Node(int x, int y) :v(x), dis(y) {}
};

void Dijkstra(int n, int s, vector<vector<Node>> Adj, vector<bool> vis, vector<int>& d)
{
       /*
       param
       n:      頂點個數
       s:      起點
       Adj:    圖的鄰接表
       vis:    標記頂點是否被訪問
       d:      儲存起點s到其他頂點的最短距離
       */
       fill(d.begin(), d.end(), INF);
       d[s] = 0;                                             //起點s到達自身的的距離為0
       for (int i = 0; i < n; ++i)
       {
              int u = -1;                                    //找到d[u]中最小的u
              int MIN = INF;                                 //找到最小的d[u]
              for (int j = 0; j < n; ++j)                    //尋找最小的d[u]
              {
                     if (vis[j] == false && d[j] < MIN)
                     {
                           u = j;
                           MIN = d[j];
                     }
              }
              //找不到小於INF的d[u],說明剩下的頂點和起點s不連通
              if (u == -1)
                     return;
              vis[u] = true;                                //標記u被訪問
              for (int j = 0; j < Adj[u].size(); ++j)
              {
                     int v = Adj[u][j].v;                   //通過鄰接表獲取u能直接到達的v
                     if (vis[v] == false && d[v] > d[u] + Adj[u][j].dis)     
                           d[v] = d[u] + Adj[u][j].dis;       //
優化d[u] } } }

時間複雜度分析:複雜度分析:主要是外層的迴圈O(V)(V就是頂點個數n)與內層迴圈(尋找最小的d[u]需要O(V)、列舉需要O(V)產生的),又由於對整個程式來說,列舉V的次數總共為 總的時間複雜度為O(V^2+E) 總結:上面的做法都是複雜度為O(V^2)級別的,其中由於必須把每個頂點都標記已訪問,因此外層迴圈的O(V)時間是無法避免的,但是尋找最小d[u]的過程卻可以不必達到O(V)的複雜度,而可以使用對優化來降低複雜度。最簡單的寫法是直接使用STL中的優先佇列priority_queue,這樣使用鄰接表實現Dijkstra演算法的時間複雜度可以降低為O(VlogV+E)。此外,Dijkstra演算法只能應對所有邊權都是非負數的情況,如果邊權出現負數,那麼Dijkstra演算法很可能會出錯,這是最好使用SPFA演算法。 (3)、如果題目給出的是無向邊(即雙向邊)而不是有向邊,又該如何解決呢?其實很簡單,只需要把無向邊當成兩條指向相反的有向邊即可。對鄰接矩陣來說,一條u與v之間的無向邊在輸入時可以分別對G[u][v]和G[v][u]賦以相同的邊權;而對於鄰接表來說,只需要在u的鄰接表Adj[u]末尾新增上v,並在v的鄰接表Adj[v]末尾新增上u即可。 (4)、前面一直是講解最短距離的求解,但是還沒有講到最短路徑本身怎麼求解。 在Dijkstra演算法的虛擬碼部分,有這麼一段:
if(v未被訪問 && 以u為中介點可以使起點s到頂點v的最短距離d[v]更優){
     優化d[v];
}
"以u為中介點可以使起點s到頂點v的最短距離d[v]更優"這句話隱含了這樣一層意思:使d[v]變得更小的方案是讓u作為從s到v最短路徑上v的前一個結點(即s->......->u->v)。於是我們不妨利用這個資訊,把這個資訊記錄下來,設定一個數組pre[],令pre[v]表示從起點s到頂點v的最短路徑上v的前一個頂點(即前驅節點)的編號,這樣就可以把每一個頂點的前驅節點記錄下來。而在虛擬碼部分只需要在if內增加一行:
if(v未被訪問 && 以u為中介點可以使起點s到頂點v的最短距離d[v]更優){
     優化d[v];
     令v的前驅為u;
}
以上圖的鄰接矩陣為例,程式碼如下:
#include <iostream>
#include <vector>
using namespace std;

const int INF = 1000000000;

/*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)
{
       /*
       param
       n:           頂點個數
       s:           源點
       G:           圖的鄰接矩陣
       vis:         標記頂點是否已被訪問
       d:           儲存源點s到達其它頂點的最短距離
       pre:         儲存從起點s到達頂點v的最短路徑上v的前一個頂點 (新新增)
       */
       fill(d.begin(), d.end(), INF);                         //初始化最短距離矩陣,全部為INF

       for (int i = 0; i < n; ++i)                            //新新增
              pre[i] = i;

       d[s] = 0;                                              //起點s到達自身的距離為0
       for (int i = 0; i < n; ++i)
       {
              int u = -1;                                     //找到d[u]最小的u
              int MIN = INF;                                  //記錄最小的d[u]
              for (int j = 0; j < n; ++j)                     //開始尋找最小的d[u]
              {
                     if (vis[j] == false && d[j] < MIN)
                     {
                           u = j;
                           MIN = d[j];
                     }
              }
              //找不到小於INF的d[u],說明剩下的頂點和起點s不連通
              if (u == -1)
                     return;
              vis[u] = true;                                  //標記u已被訪問
              for (int v = 0; v < n; ++v)
              {
                     //遍歷所有頂點,如果v未被訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
                     if (vis[v] == false && 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 << " ";
}

void main()
{
       int n = 6;
       vector<vector<int>> G = { {0,1,INF,4,4,INF},
                                 {INF,0,INF,2,INF,INF},
                                 {INF,INF,0,INF,INF,1},
                                 {INF,INF,2,0,3,INF},
                                 {INF,INF,INF,INF,0,3},
                                 {INF,INF,INF,INF,INF,0} };
       vector<bool> vis(n);
       vector<int> d(n);
       vector<int> pre(n);

       Dijkstra(n,0,G,vis,d,pre);
       for (auto x : d)
              cout << x << " ";
       cout << endl;

       //輸出從起點s到頂點v的最短路徑
       DFSPrint(0, 5, pre);
}