最短路徑:Dijkstra算法

本篇博客是我對《算法競賽入門經典》一書上內容的理解和提煉,代碼以及邏輯都是源於此書,若想更細緻的學習,還請移步原作。

:Dijkstra算法適用於邊權爲正的無向和有向圖,不適用於有負邊權的圖!(原因

用途:

    用於求圖中指定兩點之間的最短路徑,或者是指定一點到其它所有點之間的最短路徑。實質上是貪心算法

基本思想:

    1.將圖上的初始點看作一個集合S,其它點看作另一個集合

    2.根據初始點,求出其它點到初始點的距離d[i] (相鄰,則d[i]爲邊權值;若不相鄰,則d[i]爲無限大)

    3.選取最小的d[i](記爲d[x]),並將此d[i]邊對應的點(記爲x)加入集合S

    (實際上,加入集合的這個點的d[x]值就是它到初始點的最短距離

    4.再根據x,更新跟 x 相鄰點 y 的d[y]值:d[y] = min{ d[y], d[x] + 邊權值w[x][y] },因爲可能把距離調小,所以這個更新操作叫做鬆弛操作。

    (仔細想想,爲啥只更新跟x相鄰點的d[y],而不是更新所有跟集合 s 相鄰點的 d 值? 因爲第三步只更新並確定了x點到初始點的最短距離,集合內其它點是之前加入的,也經歷過第 4 步,所以與 x 沒有相鄰的點的 d 值是已經更新過的了,不會受到影響

    5.重複3,4兩步,直到目標點也加入了集合,此時目標點所對應的d[i]即爲最短路徑長度。

    (重複第三步的時候,應該從所有的d[i]中尋找最小值,而不是隻從與x點相鄰的點中尋找。想想爲什麼?

    圖解:(動圖很快,不容易理解,最好結合上面的步驟自己畫一個圖,一步一步消化)

            

    原理:這裏不進行嚴格證明,Dijkstra的大致思想就是,根據初始點,挨個的把離初始點最近的點一個一個找到並加入集合,集合中所有的點的d[i]都是該點到初始點最短路徑長度,由於後加入的點是根據集合S中的點爲基礎拓展的,所以也能找到最短路徑。

僞代碼:

清除所有點的標號;
設d[0]=0,其他d[i]=INF;//INF是一個很大的值,用來替代正無窮
循環n次 { 
在所有未標號結點中,選出d值最小的結點x;
給結點x標記;
對於從x出發的所有邊(x,y),更新d[y] = min{d[y], d[x]+w(x,y)} 
}

實現代碼:(C++)

    以下算法用於求所有點初始點的最短距離(保存在d[i]中),n是節點數,m是邊的數量。

memset(v, 0, sizeof(v)); 
for(int i = 0; i < n; i++) d[i] = (i==0 ? 0 : INF); 
for(int i = 0; i < n; i++) {  
  int x, m = INF;  
  //3.如果y沒有被加入集合,且d[y]是最小的,則把y加入集合且x = y
  for(int y = 0; y < n; y++) 
    if(!v[y] && d[y] <= m) m = d[y], x = y; 
  v[x] = 1;  //新的點加入集合(這是更新之後的新x)
  //4.更新x相鄰的點的d[i],實際上這裏更新的是所有點,但是與x未相鄰的w[x][y]值是無窮大,不可能被更新
  for(int y = 0; y < n; y++) d[y] = min(d[y], d[x] + w[x][y]); 
}

    稍微修改一下,便可以把路徑存儲起來(將當前點y的前一個點x存儲在p[y]中),之後就方便遞歸打印了。

將d[y] = min(d[y], d[x] + w[x][y])改成:

if(d[x] + w[x][y] < d[y]) {
  d[y] = d[x] + w[x][y];
  p[y] = x;

    類似於d[y] = min(d[y], d[x] + w[x][y])這種操作,它每次都可能把距離更新成更小的值,所以這類操作又叫作”鬆弛操作“。

優化代碼:(vector & 鄰接表)

    上述代碼的複雜度爲O(n^2),這裏可以用鄰接表將其優化爲O(mlogn),之所以爲優化,是因爲m往往遠小於n^2。爲了方便,我們把邊封裝在結構體中:

struct Edge {
  int from, to, dist;
  Edge(int u, int v, int d):from(u),to(v),dist(d) {}
}

    又因爲Dijkstra算法中,每次循環需要提取出最小d[i]對應的點,所以這裏可以用到優先隊列 priority_queue,隊列內部是根據d值進行排序的,又需要d值對應的點的信息(u),所以這裏可以用另一個結構體存儲放在優先隊列裏的元素(d相當於d[i],u相當於i)。

struct DistNode {
  int d, u;
  bool operator < (const HeapNode& rhs) const {
    return d > rhs.d; //這樣一來,隊列中在最頂層的是最小值
  }
}

    接下來我們再建立一個統一的鄰接表數據結構,用於接收數據並構造鄰接表,並能Dijkstra。

struct Dijkstra {
  int n, m;
  vector<Edge> edges;//相當於數組r[i],用於存儲每條邊
  vector<int> G[maxn];//鄰接表,只需存儲邊的標號即可
  book done[maxn];//用於判斷是否已經處理過該節點
  int d[maxn];距離
  int p[maxn];上一條弧
  //清除垃圾數據,初始化鄰接表和deges
  void setn(int n) {
    this->n = n;
    for(int i = 1; i <= n; i++) G[i].clear();
    edges.clear();
  }
  //添加一條邊
  void addedge(int from, int to , int dist) {
    edges.push_back(Edge(from, to, dist));
    m = edges.size();
    G[from].push_back(m - 1);
  }
  //Dijkstra
  void dijkstra(int s) {
  }
}

    接下來實現dijkstra函數:

void dijkstra(int s) {
  priority_queue<DistNode> Q;
  Q.push_back(DistNode{0, s});
  for(int i = 1; i <= n; i++) d[i] = INF;//把d[i]都設置爲最大值
  d[s] = 0;
  memset(done, 0, sizeof(done));
  while(!Q.empty()) {
    DistNode x = Q.top(); Q.pop();
    int u = x.u;
    if(done[u]) continue;//如果這個點已經被提出過了,直接拋棄(適用於那種鬆弛之後重複放入隊列的點)
    done[u] = true;
    for(int i = 0; i < Q[u].size(); i++) 
      Edge& e = edges(Q[u][i]);
      if(e.dist + d[u] < d[e.to] && d[u] < INF) {
        d[e.to] = e.dist + d[u];
        p[e.to] = G[u][i];
        Q.push_back(Edge(d[e.to], e.to));//把鬆弛過後點的d值重新放入隊列
      }
    }
  }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章