圖論——最短路:Floyd,Dijkstra,Bellman-Ford,SPFA算法及最小環問題

一.Floyd算法html

  用於計算任意兩個節點之間的最短路徑算法

        參考了five20的博客數據結構

        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(k=1;k<=n;k++) for(i=1;i<=n;i++) for(j=1;j<=n;j++) { if(dis[i][k]+dis[k][j]<dis[i][j]) { dis[i][j]=dis[i][k]+dis[k][j]; } } 

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

  

  更多關於Floyd算法,詳細請見:Floyd算法百度百科連接.net

 

二.Dijkstra算法:code

  適用於權值爲非負的圖的單源最短路徑htm

  斐波那契堆優化的時間複雜度爲O(E+VlogV),但其實只是理論值,實際中基本上達不到。
blog

  算法思路:

  參考了殷天文的博客

  • 指定一個節點,例如咱們要計算 'A' 到其餘節點的最短路徑

  • 引入兩個集合(S、U),S集合包含已求出的最短路徑的點(以及相應的最短長度),U集合包含未求出最短路徑的點(以及A到該點的路徑,注意 如上圖所示,A->C因爲沒有直接相連 初始時爲∞

  • 初始化兩個集合,S集合初始時 只有當前要計算的節點,A->A = 0

    U集合初始時爲 A->B = 4, A->C = ∞, A->D = 2, A->E = ∞敲黑板!!!接下來要的兩個步驟是核心!

  • 從U集合中找出路徑最短的點,加入S集合,例如 A->D = 2

  • 更新U集合路徑,if ( 'D 到 B,C,E 的距離' + 'AD 距離' < 'A 到 B,C,E 的距離' ) 則更新U

  • 循環執行 四、5 兩步驟,直至遍歷結束,獲得A 到其餘節點的最短路徑

  樸素版時間複雜度O(n²)算法代碼以下:

void Dijkstra() { memset(dis, 0x1f, sizeof(dis)); dis[1] = 0; for (int i=1; i<=n; i++) { int min_len = 1e9, k = 0; for (int j=1; j<=n; j++) if (!vis[j] && dis[j] < min_len) { min_len = dis[j]; k = j; } vis[k] = 1; for (int j=h[k]; j!=-1; j=edge[j].next) { int to = edge[j].to, w = edge[j].w; if (!vis[to] && dis[to] > dis[k]+w) { dis[to] = dis[k]+w; } } } }

 

  穩定時間複雜度O(mlogn)的堆優化(優先隊列代替)代碼以下:

void Dijkstra_Heap() { priority_queue<pair<int, int> > q; memset(dis, 0x3f, sizeof(dis)); memset(vis,0,sizeof(v)); dis[1] = 0; q.push(make_pair(0, 1)); while (!q.empty()) { int now = q.top().second; q.pop(); if (vis[now]) continue; vis[now] = 1; for (int i=h[now]; i!=-1; i=edge[i].next) { int to = edge[i].to, w = edge[i].w; if (!vis[to] && dis[to]>dis[now]+w) { dis[to] = dis[now] + w; q.push(make_pair(-dis[to], to)); } } } } 

這裏有一個關於優先隊列的小騷操做

  因爲STL中的優先隊列默認是大根堆,因此在使用push函數的時候只需在須要排序的數據前加個‘-’便可。

 

  關於Dijkstra算法的選擇上,對於稀疏圖,因爲n和m比較接近,故選擇堆優化算法;而對於稠密圖,因爲點少邊多,故選擇樸素版算法

 

  更多關於Dijkstra算法,詳細請見:Dijkstra算法百度百科連接

   推薦博客:數據結構--Dijkstra算法最清楚的講解

 

三.Bellman-Ford算法

  參考博客:圖解貝爾曼福特-算法

可用於解決如下問題:

  • 從A出發是否存在到達各個節點的路徑(有計算出值固然就能夠到達);
  • 從A出發到達各個節點最短路徑(時間最少、或者路徑最少等)
  • 圖中是否存在負環路(權重之和爲負數)
  • 有邊數限制的最短路

  算法思路:

  1.  初始化時將起點s到各個頂點v的距離dist(s->v)賦值爲∞,dist(s->s)賦值爲0
  2.  後續進行最多n-1次遍歷操做,對全部的邊進行鬆弛操做,假設:
  3.  所謂的鬆弛,以邊ab爲例,若dist(a)表明起點s到達a點所須要花費的總數, dist(b)表明起點s到達b點所須要花費的總數,weight(ab)表明邊ab的權重, 若存在:
  4.  (dist(a) +weight(ab)) < dist(b)
  5.  則說明存在到b的更短的路徑,s->...->a->b,更新b點的總花費爲(dist(a) +weight(ab)),父節點爲a
  6.  遍歷都結束後,若再進行一次遍歷,還能獲得s到某些節點更短的路徑的話,則說明存在負環路

  思路上與狄克斯特拉算法(Dijkstra algorithm)最大的不一樣是每次都是從源點s從新出發進行"鬆弛"更新操做,而Dijkstra則是從源點出發向外擴逐個處理相鄰的節點,不會去重複處理節點,這邊也能夠看出Dijkstra效率相對更高點。

   下面是有邊數k限制的Bellman_ford算法模板:

void Bellman_ford() { memset(dis,0x1f,sizeof(dis)); dis[1]=0; for(int i=1;i<=k;i++) { memcpy(last,dis,sizeof(last)); for(int j=1;j<=m;j++) { int u=edge[j].u,v=edge[j].v,w=edge[j].w; if(dis[v]>last[u]+w)dis[v]=last[u]+w; } } }

  

  更多關於Bellman-Ford算法,詳細請見:Bellman-Ford算法百度百科連接

 

四.SPFA算法

  SPFA是西安交通大學的段凡丁在1994年與《西安交通大學學報》中發表的「關於最短路徑的SPFA快速算法」,他在裏面說SPFA速度比Dijkstra快,且運行V次的SPFA速度比Floyd速度快。
  而事實證實SPFA算法是有侷限的,他不適用於稠密圖,對於特別狀況的稠密圖,SPFA複雜度和BellmanFord時間同樣。

  適用範圍:適用於權值有負值,且沒有負圈的圖的單源最短路徑,論文中的複雜度O(kE),k爲每一個節點進入Queue的次數,且k通常<=2,但此處的複雜度證實是有問題的,其實SPFA的最壞狀況應該是O(VE).

  注意:SPFA算法在網格圖中很是慢

  算法思路:

  參考了小天位的博客

  •  SPFA(Shortest Path Faster Algorithm) [圖的存儲方式爲鄰接表]
  •  是Bellman-Ford算法的一種隊列實現,減小了沒必要要的冗餘計算。
  •  算法大體流程是用一個隊列來進行維護。 初始時將源加入隊列。 每次從隊列中取出一個元素,並對全部與他相鄰的點進行鬆弛,若某個相鄰的點鬆弛成功,則將其入隊。 直到隊列爲空時算法結束。它能夠在O(kE)的時間複雜度內求出源點到其餘全部點的最短路徑,能夠處理負邊。
  • SPFA 在形式上和BFS很是相似,不一樣的是BFS中一個點出了隊列就不可能從新進入隊列,可是SPFA中一個點可能在出隊列以後再次被放入隊列,也就是一個點改進過其它的點以後,過了一段時間可能自己被改進,因而再次用來改進其它的點,這樣反覆迭代下去。
  •  判斷有無負環:若是某個點進入隊列的次數超過V次則存在負環(SPFA沒法處理帶負環的圖)。

  代碼以下:

void SPFA() { memset(dis, 0x3f, sizeof(dis)); memset(vis, 0, sizeof(vis)); queue<int> q; dis[1] = 0; vis[1] = 1; q.push(1); while (!q.empty()) { int now = q.front(); q.pop(); vis[now] = 0; for (int i=h[now]; i!=-1; i=edge[i].next) { int to = edge[i].to, w = edge[i].w; if (dis[now]+w < dis[to]) {  //在隊列中的點也能夠進行鬆弛操做,故這裏不需加!ifq[to]的條件,而須要加在下面。
                dis[to] = dis[now]+w; if (!vis[to]) q.push(to), vis[to] = 1; } } } }

 

  更多關於SPFA算法,詳細請見:SPFA算法百度百科連接

 

五.最小環問題

  解決思路:

  最小環就是指在一張圖中找出一個環,使得這個環上的各條邊的權值之和最小。在Floyed的同時,能夠順便算出最小環。

  記兩點間的最短路爲dis[i][j],g[i][j]爲邊<i,j>的權值。 

  一個環中的最大結點爲k(編號最大),與它相連的兩個點爲i,j,這個環的最短長度爲g[i][k]+g[k][j]+(i到j的路徑中,全部結點編號都小於k的最短路徑長度)。

  根據Floyed的原理,在最外層循環作了k-1次以後,dis[i][j]則表明了i到j的路徑中,全部結點編號都小於k的最短路徑。

  綜上所述,該算法必定能找到圖中最小環。

  代碼以下:

  參考了Coder_YX的博客

void floyd(){ int MinCost = inf; for(int k=1;k<=n;k++){ for(int i=1;i<k;i++) for(int j=i+1;j<k;j++) MinCost = min(MinCost,dis[i][j]+mp[i][k]+mp[k][j]);//更新k點以前枚舉ij求通過ijk的最小環
         for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);      //更新k點
 } if(MinCost==inf)puts("It's impossible."); else printf("%d\n",MinCost); }

 

關於四種最短路算法的結論:

 參考了xiazdong的博客

(1)當權值爲非負時,用Dijkstra。
(2)當權值有負值,且沒有負圈,則用SPFA,SPFA能檢測負圈,可是不能輸出負圈。
(3)當權值有負值,並且可能存在負圈,則用BellmanFord,可以檢測並輸出負圈。
(4)SPFA檢測負環:當存在一個點入隊大於等於V次,則有負環。

相關文章
相關標籤/搜索