最短路徑的求解

一.問題引入html

        問題:從某頂點出發,沿圖的邊到達另外一頂點所通過的路徑中,各邊上權值之和最小的一條路徑——最短路徑。解決最短路的問題有如下算法,Dijkstra算法,Bellman-Ford算法,Floyd算法和SPFA算法,另外還有著名的啓發式搜索算法A*,不過A*準備單獨出一篇,其中Floyd算法能夠求解任意兩點間的最短路徑的長度。筆者認爲任意一個最短路算法都是基於這樣一個事實:從任意節點A到任意節點B的最短路徑不外乎2種可能,1是直接從A到B,2是從A通過若干個節點到B。node

二.Dijkstra算法ios

        該算法在《數據結構》課本里是以貪心的形式講解的,不過在《運籌學》教材裏被編排在動態規劃章節,建議讀者兩篇都看看。算法

           image

        觀察右邊表格發現除最後一個節點外其餘均已經求出最短路徑。數組

        (1)   迪傑斯特拉(Dijkstra)算法按路徑長度(看下面表格的最後一行,就是next點)遞增次序產生最短路徑。先把V分紅兩組:安全

  • S:已求出最短路徑的頂點的集合
  • V-S=T:還沒有肯定最短路徑的頂點集合

        將T中頂點按最短路徑遞增的次序加入到S中,依據:能夠證實V0到T中頂點Vk的最短路徑,或是從V0到Vk的直接路徑的權值或是從V0經S中頂點到Vk的路徑權值之和(反證法可證,說實話,真不明白哦)。服務器

        (2)   求最短路徑步驟數據結構

  1. 初使時令 S={V0},T={其他頂點},T中頂點對應的距離值, 若存在<V0,Vi>,爲<V0,Vi>弧上的權值(和SPFA初始化方式不一樣),若不存在<V0,Vi>,爲Inf。
  2. 從T中選取一個其距離值爲最小的頂點W(貪心體如今此處),加入S(注意不是直接從S集合中選取,理解這個對於理解vis數組的做用相當重要),對T中頂點的距離值進行修改:若加進W做中間頂點,從V0到Vi的距離值比不加W的路徑要短,則修改此距離值(上面兩個並列for循環,使用最小點更新)。
  3. 重複上述步驟,直到S中包含全部頂點,即S=V爲止(說明最外層是除起點外的遍歷)。

        下面是上圖的求解過程,按列來看,第一列是初始化過程,最後一行是每次求得的next點。工具

           image

        (3)   問題:Dijkstar可否處理負權邊?(來自《圖論》)測試

             答案是不能,這與貪心選擇性質有關(ps:貌似仍是動態規劃啊,暈了),每次都找一個距源點最近的點(dmin),而後將該距離定爲這個點到源點的最短路徑;但若是存在負權邊,那就有可能先經過並非距源點最近的一個次優勢(dmin'),再經過這個負權邊L(L<0),使得路徑之和更小(dmin'+L<dmin),則dmin'+L成爲最短路徑,並非dmin,這樣dijkstra就被囧掉了。好比n=3,鄰接矩陣: 
0,3,4 
3,0,-2 
4,-2,0,用dijkstra求得d[1,2]=3,事實上d[1,2]=2,就是經過了1-3-2使得路徑減少。不知道講得清楚不清楚。

二.Floyd算法

        參考了南陽理工牛帥(目前在新浪)的博客。

        Floyd算法的基本思想以下:從任意節點A到任意節點B的最短路徑不外乎2種可能,1是直接從A到B,2是從A通過若干個節點到B,因此,咱們假設dist(AB)爲節點A到節點B的最短路徑的距離,對於每個節點K,咱們檢查dist(AK) + dist(KB) < dist(AB)是否成立,若是成立,證實從A到K再到B的路徑比A直接到B的路徑短,咱們便設置 dist(AB) = dist(AK) + dist(KB),這樣一來,當咱們遍歷完全部節點K,dist(AB)中記錄的即是A到B的最短路徑的距離。

        很簡單吧,代碼看起來可能像下面這樣:

for (int i=0; i<n; ++i) {
  for (int j=0; j<n; ++j) {
    for (int k=0; k<n; ++k) {
      if (dist[i][k] + dist[k][j] < dist[i][j] ) {
        dist[i][j] = dist[i][k] + dist[k][j];
      }
    }
  }
}

        可是這裏咱們要注意循環的嵌套順序,若是把檢查全部節點K放在最內層,那麼結果將是不正確的,爲何呢?由於這樣便過早的把i到j的最短路徑肯定下來了,而當後面存在更短的路徑時,已經再也不會更新了。

        讓咱們來看一個例子,看下圖:

image

        圖中紅色的數字表明邊的權重。若是咱們在最內層檢查全部節點K,那麼對於A->B,咱們只能發現一條路徑,就是A->B,路徑距離爲9,而這顯然是不正確的,真實的最短路徑是A->D->C->B,路徑距離爲6。形成錯誤的緣由就是咱們把檢查全部節點K放在最內層,形成過早的把A到B的最短路徑肯定下來了,當肯定A->B的最短路徑時dist(AC)還沒有被計算。因此,咱們須要改寫循環順序,以下:

        ps:我的以爲,這和銀行家算法判斷安全狀態(每種資源去測試全部線程),樹狀數組更新(更新全部相關項)同樣的思想。

for (int k=0; k<n; ++k) {
  for (int i=0; i<n; ++i) {
    for (int j=0; j<n; ++j) {
            /*
            實際中爲防止溢出,每每須要選判斷 dist[i][k]和dist[k][j
            都不是Inf ,只要一個是Inf,那麼就確定沒必要更新。 
            */
      if (dist[i][k] + dist[k][j] < dist[i][j] ) {
        dist[i][j] = dist[i][k] + dist[k][j];
      }
    }
  }
}

        若是仍是看不懂,那就用草稿紙模擬一遍,以後你就會豁然開朗。半個小時足矣(早知道的話會節省不少個半小時了。。狡猾

       再來看路徑保存問題:

void floyd() {
      for(int i=1; i<=n ; i++){
        for(int j=1; j<= n; j++){
          if(map[i][j]==Inf){
               path[i][j] = -1;//表示  i -> j 不通 
          }else{
               path[i][j] = i;// 表示 i -> j 前驅爲 i
          }
        }
      }
      for(int k=1; k<=n; k++) {
        for(int i=1; i<=n; i++) {
          for(int j=1; j<=n; j++) {
            if(!(dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j] > dist[i][k] + dist[k][j]) {
              dist[i][j] = dist[i][k] + dist[k][j];
              //path[i][k] = i;//刪掉
              path[i][j] = path[k][j];
            }
          }
        }
      }
    }
    void printPath(int from, int to) {
        /*
         * 這是倒序輸出,若想正序可放入棧中,而後輸出。
         * 
         * 這樣的輸出爲何正確呢?我的認爲用到了最優子結構性質,
         * 即最短路徑的子路徑仍然是最短路徑
         */
        while(path[from][to]!=from) {
            System.out.print(path[from][to] +"");
            to = path[from][to];
        }
    }

        《數據結構》課本上的那種方式我如今仍是不想看,看着不舒服……

        Floyd算法另外一種理解DP,爲理論愛好者準備的,上面這個形式的算法實際上是Floyd算法的精簡版,而真正的Floyd算法是一種基於DP(Dynamic Programming)的最短路徑算法。設圖G中n 個頂點的編號爲1到n。令c [i, j, k]表示從i 到j 的最短路徑的長度,其中k 表示該路徑中的最大頂點,也就是說c[i,j,k]這條最短路徑所經過的中間頂點最大不超過k。所以,若是G中包含邊<i, j>,則c[i, j, 0] =邊<i, j> 的長度;若i= j ,則c[i,j,0]=0;若是G中不包含邊<i, j>,則c (i, j, 0)= +∞。c[i, j, n] 則是從i 到j 的最短路徑的長度。對於任意的k>0,經過分析能夠獲得:中間頂點不超過k 的i 到j 的最短路徑有兩種可能:該路徑含或不含中間頂點k。若不含,則該路徑長度應爲c[i, j, k-1],不然長度爲 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取二者中的最小值。狀態轉移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。這樣,問題便具備了最優子結構性質,能夠用動態規劃方法來求解。

        看另外一個DP(直接引用王老師課件)

                       image

 

        說了這麼多,相信讀者已經躍躍欲試了,我們看一道例題,以ZOJ 1092爲例:給你一組國家和國家間的部分貨幣匯率兌換表,問你是否存在一種方式,從一種貨幣出發,通過一系列的貨幣兌換,最後返回該貨幣時大於出發時的數值(ps:這就是所謂的投機倒把吧),下面是一組輸入。 
3    //國家數 
USDollar  //國家名 
BritishPound 
FrenchFranc 
   3    //貨幣兌換數 
USDollar 0.5 BritishPound  //部分貨幣匯率兌換表 
BritishPound 10.0 FrenchFranc 
FrenchFranc 0.21 USDollar

        月賽作的題,不過當時用的思路是求強連通份量(ps:明明說的,那時我和華傑感受好有道理),也沒作出來,如今知道了直接floyd算法就ok了。

        思路分析:輸入的時候能夠採用Map<String,Integer> map = new HashMap<String,Integer>()主要是爲了得到再次包含匯率輸入時候的下標以建圖(感受本身寫的好拗口),或者第一次直接存入String數組str,再次輸入的時候每次遍歷str數組,如果equals那麼就把str的下標賦值給該幣種建圖。下面就是floyd算法啦,初始化其它點爲-1,對角線爲1,採用乘法更新求最大值。

三.Bellman-Ford算法

        爲了可以求解邊上帶有負值的單源最短路徑問題,Bellman(貝爾曼,動態規劃提出者)和Ford(福特)提出了從源點逐次繞過其餘頂點,以縮短到達終點的最短路徑長度的方法。 Bellman-ford算法是求含負權圖的單源最短路徑算法,效率很低,但代碼很容易寫。即進行不停地鬆弛,每次鬆弛把每條邊都更新一下,若n-1次鬆弛後還能更新,則說明圖中有負環,沒法得出結果,不然就成功完成。Bellman-ford算法有一個小優化:每次鬆弛先設一個flag,初值爲FALSE,如有邊更新則賦值爲TRUE,最終若是仍是FALSE則直接成功退出。Bellman-ford算法浪費了許多時間作無必要的鬆弛,因此SPFA算法用隊列進行了優化,效果十分顯著,高效不可思議。SPFA還有SLF,LLL,滾動數組等優化。

        關於SPFA,請看我這一篇http://www.cnblogs.com/hxsyl/p/3248391.html

        遞推公式(求頂點u到源點v的最短路徑):

         dist 1 [u] = Edge[v][u]

         dist k [u] = min{ dist k-1 [u], min{ dist k-1 [j] + Edge[j][u] } }, j=0,1,…,n-1,j≠u

         Dijkstra算法和Bellman算法思想有很大的區別:Dijkstra算法在求解過程當中,源點到集合S內各頂點的最短路徑一旦求出,則以後不變了,修改  的僅僅是源點到T集合中各頂點的最短路徑長度。Bellman算法在求解過程當中,每次循環都要修改全部頂點的dist[ ],也就是說源點到各頂點最短路徑長度一直要到Bellman算法結束才肯定下來。

        算法適用條件

  • 1.單源最短路徑(從源點s到其它全部頂點v)
  • 有向圖&無向圖(無向圖能夠看做(u,v),(v,u)同屬於邊集E的有向圖)
  • 邊權可正可負(若有負權迴路輸出錯誤提示)
  • 差分約束系統(至今貌似只看過一道題)

        Bellman-Ford算法描述:

  1. 初始化:將除源點外的全部頂點的最短距離估計值 d[v] ←+∞, d[s] ←0
  2. 迭代求解:反覆對邊集E中的每條邊進行鬆弛操做,使得頂點集V中的每一個頂點v的最短距離估計值逐步逼近其最短距離;(運行|v|-1次,看下面的描述性證實(當作樹))
  3. 檢驗負權迴路:判斷邊集E中的每一條邊的兩個端點是否收斂。若是存在未收斂的頂點,則算法返回false,代表問題無解;不然算法返回true,而且從源點可達的頂點v的最短距離保存在d[v]中

        描述性證實:(這個解釋很好)

        首先指出,圖的任意一條最短路徑既不能包含負權迴路,也不會包含正權迴路,所以它最多包含|v|-1條邊。

其次,從源點s可達的全部頂點若是 存在最短路徑,則這些最短路徑構成一個以s爲根的最短路徑樹。Bellman-Ford算法的迭代鬆弛操做,實際上就是按頂點距離s的層次,逐層生成這棵最短路徑樹的過程。

在對每條邊進行1遍鬆弛的時候,生成了從s出發,層次至多爲1的那些樹枝。也就是說,找到了與s至多有1條邊相聯的那些頂點的最短路徑;對每條邊進行第2遍鬆弛的時候,生成了第2層次的樹枝,就是說找到了通過2條邊相連的那些頂點的最短路徑……。由於最短路徑最多隻包含|v|-1條邊,因此,只須要循環|v|-1 次。

每實施一次鬆弛操做,最短路徑樹上就會有一層頂點達到其最短距離,此後這層頂點的最短距離值就會一直保持不變,再也不受後續鬆弛操做的影響。(可是,每次還要判斷鬆弛,這裏浪費了大量的時間,這就是Bellman-Ford算法效率底下的緣由,也正是SPFA優化的所在)。

image,如圖(沒找到畫圖工具的射線),如果B和C的最短路徑不更新,那麼點D的最短路徑確定也沒法更新,這就是優化所在。

若是沒有負權迴路,因爲最短路徑樹的高度最多隻能是|v|-1,因此最多通過|v|-1遍鬆弛操做後,全部從s可達的頂點必將求出最短距離。若是 d[v]仍保持 +∞,則代表從s到v不可達。

若是有負權迴路,那麼第 |v|-1 遍鬆弛操做仍然會成功,這時,負權迴路上的頂點不會收斂。

           參考了《圖論》。

        問題:Bellman-Ford算法是否必定要循環n-1次麼?未必!其實只要在某次循環過程當中,考慮每條邊後,都沒能改變當前源點到全部頂點的最短路徑長度,那麼Bellman-Ford算法就能夠提早結束了(開篇提出的小優化就是這個)。

        上代碼(參考了牛帥的博客)

#include<iostream>
#include<cstdio>
using namespace std;
 
 
#define MAX 0x3f3f3f3f
#define N 1010
int nodenum, edgenum, original; //點,邊,起點
 
 
typedef struct Edge //邊
{
  int u, v;
  int cost;
}Edge;
 
 
Edge edge[N];
int dis[N], pre[N];
 
 
bool Bellman_Ford()
{
  for(int i = 1; i <= nodenum; ++i) //初始化
    dis[i] = (i == original ? 0 : MAX);
  for(int i = 1; i <= nodenum - 1; ++i)
    for(int j = 1; j <= edgenum; ++j)
      if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //鬆弛(順序必定不能反~)
      {
        dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
        pre[edge[j].v] = edge[j].u;
      }
      bool flag = 1; //判斷是否含有負權迴路
      for(int i = 1; i <= edgenum; ++i)
        if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
        {
          flag = 0;
          break;
        }
        return flag;
}
 
 
void print_path(int root) //打印最短路的路徑(反向)
{
  while(root != pre[root]) //前驅
  {
    printf("%d-->", root);
    root = pre[root];
  }
  if(root == pre[root])
    printf("%d\n", root);
}
 
 
int main()
{
  scanf("%d%d%d", &nodenum, &edgenum, &original);
  pre[original] = original;
  for(int i = 1; i <= edgenum; ++i)
  {
    scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
  }
  if(Bellman_Ford())
    for(int i = 1; i <= nodenum; ++i) //每一個點最短路
    {
      printf("%d\n", dis[i]);
      printf("Path:");
      print_path(i);
    }
  else
    printf("have negative circle\n");
  return 0;
}

四.SPFA算法

        用一個隊列來進行維護。初始時將源加入隊列。每次從隊列中取出一個元素,並對全部與他相鄰的點進行鬆弛,若某個相鄰的點鬆弛成功,則將其入隊。直到隊列爲空時算法結束;這個算法,簡單的說就是隊列優化的bellman-ford,利用了每一個點不會更新次數太多的特色發明的此算法(看我上面那個圖,只有相鄰點更新了,該點纔有可能更新) 。

         代碼參見 : http://www.cnblogs.com/hxsyl/p/3248391.html

五.趣聞

        整理該篇博文的時候,一哥們發佈網站到某羣,網站很精美,一牛神(acmol)使用fork炸彈,結果服務器立馬掛啦,更改後又掛啦,目測目前無限掛中。。。

相關文章
相關標籤/搜索