1. 程式人生 > >演算法(五):圖解貝爾曼-福特演算法

演算法(五):圖解貝爾曼-福特演算法

演算法簡介

貝爾曼-福特演算法(Bellman–Ford algorithm )用於計算出起點到各個節點的最短距離,支援存在負權重的情況

  • 它的原理是對圖進行最多V-1次鬆弛操作,得到所有可能的最短路徑。其優於迪科斯徹演算法的方面是邊的權值可以為負數、實現簡單,缺點是時間複雜度過高,高達O(VE)。但演算法可以進行若干種優化,提高了效率。

  • Bellman Ford演算法每次對所有的邊進行鬆弛,每次鬆弛都會得到一條最短路徑,所以總共需要要做的鬆弛操作是V - 1次。在完成這麼多次鬆弛後如果還是可以鬆弛的話,那麼就意味著,其中包含負環。

  • 相比狄克斯特拉演算法(Dijkstra algorithm)

    ,其最大優點便是Bellman–Ford支援存在負權重的情況,並且程式碼實現相對簡單。缺點便是時間複雜度較高,達到O(V*E),V代表頂點數,E代表邊數。

可用於解決以下問題:

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

其思路為:

  1. 初始化時將起點s到各個頂點v的距離dist(s->v)賦值為∞,dist(s->s)賦值為0

  2. 後續進行最多n-1次遍歷操作(n為頂點個數,上標的v輸入法打不出來...),對所有的邊進行鬆弛操作,假設:

    所謂的鬆弛,以邊ab為例,若dist(a)代表起點s到達a點所需要花費的總數, dist(b)代表起點s到達b點所需要花費的總數,weight(ab)代表邊ab的權重, 若存在:

    (dist(a) +weight(ab)) < dist(b)

    則說明存在到b的更短的路徑,s->...->a->b,更新b點的總花費為(dist(a) +weight(ab)),父節點為a

  3. 遍歷都結束後,若再進行一次遍歷,還能得到s到某些節點更短的路徑的話,則說明存在負環路

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

案例

案例一

先舉個網上常見的例子介紹其實現的思路:

如下圖按Bellman–Ford演算法思路獲取起點A到終點的最短路徑

由上介紹可知,由於該圖頂點總數n=5個頂點,所以需要進行5-1 = 4 次的遍歷更新操作,每次操作若能發現更短的路徑則更新對應節點的值

1.首先建立邊物件資訊,需要按從源點A出發,由近到遠的順序,不然沒從源點開始的話dist(s)==∞無窮大會增加後續計算的麻煩:

AB:-1
AC:4
BC:3
BE:2
BD:2
ED:-3
DC:5
DB:1
複製程式碼

1.首先列出起點A到各個節點耗費的時間:

父節點 節點 初始化
A A 0
.. B
.. C
.. D
.. E

2.進行第一次對所有邊進行的鬆弛操作:

2.1統計經過1條邊所能到達的節點的值AB,AC:

AB:-1
AC:4
複製程式碼
父節點 節點 耗費
A A 0
A B -1
A C 4
.. D
.. E

2.2統計經過2條邊所能到達的節點的值BC,BD,BE:

BC:3
BE:2
BD:2
複製程式碼
父節點 節點 耗費
A A 0
A B -1
B C 2
B D 1
B E 1

以節點C為例,因為滿足: dist(B) + weight(BC) > dist(C),即 -1 + 3 >4,所以C更新為2

2.3統計經過3條邊所能到達的節點的值ED,DC:

ED:-3
DC:5
DB:1
複製程式碼
父節點 節點 耗費
A A 0
A B -1
B C 2
E D -2
B E 1

3.嘗試再進行第2次遍歷,對所有邊進行鬆弛操作,發現沒有節點需要進行更新,此時便可以提前結束遍歷,優化效率

父節點 節點 耗費
A A 0
A B -1
B C 2
E D -2
B E 1

4.由上表可知,此時便求出了源點A到各個節點的最短路徑與線路

案例二

如下圖,求出A到各節點的最短路徑

1.該圖共有節點7個,最多需要進行7-1=6次的對所有邊的鬆弛操作

2.首先建立邊物件:

AB:6
AC:5
AD:5
CB:-2
DC:-2
BE:-1
CE:1
DF:-1
EG:3
FG:3
複製程式碼

3.進行第一次遍歷鬆弛操作,可以得到:

父節點 節點 耗費
A A 0
C B 3
D C 3
A D 5
B E 2
D F 4
E G 5

4.進行第二次遍歷鬆弛操作,得到:

父節點 節點 耗費
A A 0
C B 1
D C 3
A D 5
B E 0
D F 4
E G 3

5.進行第三次遍歷鬆弛操作,結果並沒有再次更新:

父節點 節點 耗費
A A 0
C B 1
D C 3
A D 5
B E 0
D F 4
E G 3

6.此時上表邊上A到各個節點的最短路徑,可以通過倒序的方式得出路線

8.這邊假設同級邊物件(指的是從A出發,經過相同的邊數可以到達的,比如DC,CB,BE,DF,CE經過2條邊就可以到達)排序位置進行調整,並不會影響結果的正確性,但是會影響所需要的遍歷的次數(不同級別):

比如上述,AB:6 ,AC:5,AD:5,CB:-2,DC:-2,BE:-1,CE:1,DF:-1,EG:3,FG:3 程式碼需要遍歷3次才可以確認結果(最後一次用於確認結果不再更新);

AB:6,AC:5,AD:5,DC:-2,CB:-2,BE:-1,CE:1,DF:-1,EG:3,FG:3 程式碼需要遍歷2次就可以確認結果;

AB:6,AC:5,AD:5,BE:-1,CE:1,DF:-1,DC:-2,CB:-2,EG:3,FG:3 程式碼需要遍歷4次就可以確認結果;

有時候圖的關係是使用者輸入的,對於順序並不好強制一定是最佳的

侷限性

案例三,存在負環路的情況

  • 對案例一的圖進行修改B->D為-2.使得B<->D這形成了負環路,所謂的負環路指的是環路權重之和為負數,比如上圖 1 + (-2) = -1 < 0即為負環路。

  • 因為負環路可以無限執行迴圈步驟,只要你想,可以在 B->D->B->D...這邊無限迴圈,所以B、D的取值可以無限小, 然後當B、D取值無限小後再從B、D節點出發到達其他各個節點,都會導致其它節點的取值同樣接近無限小,所以對於負環路的情況,Bellman–Ford只能判斷出圖存在負環路,而沒有求出各個節點最短路徑的意義

  • Bellman–Ford求出的各個節點的最短路徑後,可以再進行一次遍歷,就可以判斷出是否存在負環路。

例如,同案例一,對該圖執行4次遍歷後得到結果:

父節點 節點 耗費 執行線路
A A 0
A B -2 A->B->D->B
B C 1 A->B->D->B->C
E D -4 A->B->D-B->D
B E 0 A->B->D->B->E

此時結束後,所得到的結果並非是正確的最短路徑,比如再進行一次遍歷,更新從源點A出發,經過5條邊所能到達的節點的值

父節點 節點 耗費 執行線路
A A 0
A B -3 A->B->D->B->D->B
B C 1 A->B->D->B->C
E D -4 A->B->D-B->D
B E 0 A->B->D->B->E

發現此時存在可以更新的節點B,則證明圖中存在了負環路。

當沒限制次數時,則無法得出各個節點的最短路徑,若人為限制了遍歷次數,則可以找出源點到各個節點的最短路徑

小結

1.廣度優先演算法BFS主要適用於無權重向圖重搜尋出源點到終點的步驟最少的路徑,當方向圖存在權重時,不再適用

2.狄克斯特拉演算法Dijkstra主要用於有權重的方向圖中搜索出最短路徑,但不適合於有負權重的情況.對於環圖,個人感覺和BFS一樣,標誌好已處理的節點避免進入死迴圈,可以支援

3.貝爾曼-福特演算法Bellman–Ford主要用於存在負權重的方向圖中(沒有負權重也可以用,但是效率比Dijkstra低很多),搜尋出源點到各個節點的最短路徑

4.Bellman–Ford可以判斷出圖是否存在負環路,但存在負環路的情況下不支援計算出各個節點的最短路徑。只需要在結束(節點數目-1)次遍歷後,再執行一次遍歷,若還可以更新資料則說明存在負環路

5.當人為限制了遍歷次數後,對於負環路也可以計算出,但似乎沒啥實際意義

java實現

/**
 * 貝爾曼-福特演算法
 * @author Administrator
 *
 */
public class BellmanFord {
	public static void main(String[] args){
		
		//建立圖
		Edge ab = new Edge("A", "B", -1);
		Edge ac = new Edge("A", "C", 4);
		Edge bc = new Edge("B", "C", 3);
		Edge be = new Edge("B", "E", 2);
		Edge ed = new Edge("E", "D", -3);
		Edge dc = new Edge("D", "C", 5);
		Edge bd = new Edge("B", "D", 2);
		Edge db = new Edge("D", "B", 1);
		
		//需要按圖中的步驟步數順序建立陣列,否則就是另外一幅圖了,
		//從起點A出發,步驟少的排前面
		Edge[] edges = new Edge[] {ab,ac,bc,be,bd,ed,dc,db};
		
		//存放到各個節點所需要消耗的時間
		HashMap<String,Integer> costMap = new HashMap<String,Integer>();
		//到各個節點對應的父節點
		HashMap<String,String> parentMap = new HashMap<String,String>();
		
		
		//初始化各個節點所消費的,當然也可以再遍歷的時候判斷下是否為Null
		//i=0的時候
		costMap.put("A", 0); //源點
		costMap.put("B", Integer.MAX_VALUE);
		costMap.put("C", Integer.MAX_VALUE);
		costMap.put("D", Integer.MAX_VALUE);
		costMap.put("E", Integer.MAX_VALUE);
		
		//進行節點數n-1次迴圈
		for(int i =1; i< costMap.size();i++) {
			boolean hasChange = false;
			for(int j =0; j< edges.length;j++) {
				Edge edge = edges[j];
				//該邊起點目前總的路徑大小
				int startPointCost = costMap.get(edge.getStartPoint()) == null ? 0:costMap.get(edge.getStartPoint());
				//該邊終點目前總的路徑大小
				int endPointCost = costMap.get(edge.getEndPoint()) == null ? Integer.MAX_VALUE : costMap.get(edge.getEndPoint());
				//如果該邊終點目前的路徑大小 > 該邊起點的路徑大小 + 該邊權重 ,說明有更短的路徑了
				if(endPointCost > (startPointCost + edge.getWeight())) {
					costMap.put(edge.getEndPoint(), startPointCost + edge.getWeight());
					parentMap.put(edge.getEndPoint(), edge.getStartPoint());
					hasChange = true;
				}
			}
			if (!hasChange) {
				//經常還沒達到最大遍歷次數便已經求出解了,此時可以優化為提前退出迴圈
				break;
			}
		}
		
		//在進行一次判斷是否存在負環路
		boolean hasRing = false;
		for(int j =0; j< edges.length;j++) {
			Edge edge = edges[j];
			int startPointCost = costMap.get(edge.getStartPoint()) == null ? 0:costMap.get(edge.getStartPoint());
			int endPointCost = costMap.get(edge.getEndPoint()) == null ? Integer.MAX_VALUE : costMap.get(edge.getEndPoint());
			if(endPointCost > (startPointCost + edge.getWeight())) {
				System.out.print("\n圖中存在負環路,無法求解\n");
				hasRing = true;
				break;
			}
		}
		
		if(!hasRing) {
			//打印出到各個節點的最短路徑
			for(String key : costMap.keySet()) {
				System.out.print("\n到目標節點"+key+"最低耗費:"+costMap.get(key));
				if(parentMap.containsKey(key)) {
					List<String> pathList = new ArrayList<String>();
					String parentKey = parentMap.get(key);
					while (parentKey!=null) {
						pathList.add(0, parentKey);
						parentKey = parentMap.get(parentKey);
					}
					pathList.add(key);
					String path="";
					for(String k:pathList) {
						path = path.equals("") ? path : path + " --> ";
						path = path +  k ;
					}
					System.out.print(",路線為"+path);
				} 
			}
		}

		
	}
	

	
	/**
	 * 	代表"一條邊"的資訊物件
	 * 
	 * @author Administrator
	 *
	 */
	static class Edge{
		//起點id
		private String startPoint;
		//結束點id
		private String endPoint;
		//該邊的權重
		private int weight;
		public Edge(String startPoint,String endPoint,int weight) {
			this.startPoint = startPoint;
			this.endPoint = endPoint;
			this.weight = weight;
		}
		public String getStartPoint() {
			return startPoint;
		}
		
		public String getEndPoint() {
			return endPoint;
		}
		
		public int getWeight() {
			return weight;
		}
	}
}
複製程式碼
執行完main方法列印資訊如下:
到目標節點B最低耗費:-1,路線為A --> B
到目標節點C最低耗費:2,路線為A --> B --> C
到目標節點D最低耗費:-2,路線為A --> B --> E --> D
到目標節點E最低耗費:1,路線為A --> B --> E
複製程式碼

微信公眾號

掃描關注獲取最新內容


作者:CodeInfo
連結:https://juejin.im/post/5b77fec1e51d4538cf53be68
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關推薦

演算法():圖解-演算法

演算法簡介 貝爾曼-福特演算法(Bellman–Ford algorithm )用於計算出起點到各個節點的最短距離,支援存在負權重的情況 它的原理是對圖進行最多V-1次鬆弛操作,得到所有可能的最短路徑。其優於迪科斯徹演算法的方面是邊的權值可以為負數、實現簡單,缺點是時

Bellman ford演算法·演算法

Bellman - ford演算法是求含負權圖的單源最短路徑的一種演算法,效率較低,程式碼難度較小。其原理為連續進行鬆弛,在每次鬆弛時把每條邊都更新一下,若在n-1次鬆弛後還能更新,則說明圖中有負環,因此無法得出結果,否則就完成。 問題集錦:

演算法

//Bellman_ford #include<iostream> using namespace std; struct Edge{ int v,e,weight; }a[1000]; int dist[1000]; int node,edge,source

Bellman-ford(-演算法)解析

Dijkstra演算法是處理單源最短路徑的有效演算法,但它侷限於邊的權值非負的情況,若圖中出現權值為負的邊,Dijkstra演算法就會失效,求出的最短路徑就可能是錯的。 這時候,就需要使用其他的演算法來求解最短路徑,Bellman-Ford演算法就是其中最常用的一個。該

-(Bellman-Ford)演算法——解決負權邊(C++實現)

Dijkstra演算法雖然好,但是它不能解決帶有負權邊(邊的權值為負數)的圖。 接下來學習一種無論在思想上還是在程式碼實現上都可以稱為完美的最短路徑演算法:Bellman-Ford演算法。 Bellman-Ford演算法非常簡單,核心程式碼四行,可以完美的解決帶有負權邊的圖。 for(k

最短路徑 Floyd演算法 Dijkstra演算法 Bellman-Ford(演算法

相信大家應該對最短路徑演算法很感興趣吧!不感興趣也沒關係,我們一起來看看下面的例子。最短路徑應該是在眾多演算法中。最常用的一類演算法。為什麼這樣說呢?? 例如: 1.乘汽車旅行的人總希望找出到目的地的儘可能的短的行

有權最短路徑問題:德(Bellman Ford)演算法 & Java 實現

一、貝爾曼福德演算法 1. 簡介 貝爾曼福德(Bellman Ford)演算法也是求解單源最短路徑問題,相比狄克斯特拉(dijkstra)演算法,它執行效率會差一些,但是它可以處理邊的權重為負值的情況,而狄克斯特拉演算法要求變的權重不能為負數。 2. 演算法思

通俗理解卡濾波及其演算法實現(帶例項解析)

1.簡介(Brief Introduction) 在學習卡爾曼濾波器之前,首先看看為什麼叫“卡爾曼”。跟其他著名的理論(例如傅立葉變換,泰勒級數等等)一樣,卡爾曼也是一個人的名字,而跟他們不同的是,他是個現代人! 卡爾曼全名Rudolf Emil Kalman,匈牙利數學家,1930年出生於

排序演算法4——圖解排序及其實現

排序演算法1——圖解氣泡排序及其實現(三種方法,基於模板及函式指標) 排序演算法2——圖解簡單選擇排序及其實現 排序演算法3——圖解直接插入排序以及折半(二分)插入排序及其實現 排序演算法4——圖解希爾排序及其實現 排序演算法5——圖解堆排序及其實現 排序演算法6——圖解歸併排序及其遞迴與非

可夫與方程學習筆記

馬爾可夫決策的要求: 1,能夠檢測到理想狀態:比如我們想讓強化學習演算法來實現走迷宮,最後這個迷宮一定是可以走出的,倘若迷宮沒有出口便不可以套用馬爾可夫。 2,可以多次嘗試:依然使用走迷宮的例子,我們可以在走迷宮失敗的時候進行多次嘗試,而不是失敗以後就停止。 3,系

[轉]通俗理解卡濾波及其演算法實現(例項解析)

1.簡介(Brief Introduction)在學習卡爾曼濾波器之前,首先看看為什麼叫“卡爾曼”。跟其他著名的理論(例如傅立葉變換,泰勒級數等等)一樣,卡爾曼也是一個人的名字,而跟他們不同的是,他是個現代人!卡爾曼全名Rudolf Emil Kalman,匈牙利數學家,19

通俗理解卡濾波及其演算法實現(例項解析)

1.簡介(Brief Introduction)在學習卡爾曼濾波器之前,首先看看為什麼叫“卡爾曼”。跟其他著名的理論(例如傅立葉變換,泰勒級數等等)一樣,卡爾曼也是一個人的名字,而跟他們不同的是,他是個現代人!卡爾曼全名Rudolf Emil Kalman,匈牙利數學家,19

一個應用例項詳解卡濾波及其演算法實現

為了可以更加容易的理解卡爾曼濾波器,這裡會應用形象的描述方法來講解,而不是像大多數參考書那樣羅列一大堆的數學公式和數學符號。但是,他的5條公式是其核心內容。結合現代的計算機,其實卡爾曼的程式相當的簡單,只要你理解了他的那5條公式。在介紹他的5條公式之前,先讓我們來根據下面的例子一步一步的探索。假設我們要研究的

HDU 2512 一卡通大冒險(第二類斯林數+數)

題目連結:http://acm.hdu.edu.cn/showproblem.php?pid=2512 題目大意:因為長期鑽研演算法, 無暇顧及個人問題,BUAA ACM/ICPC 訓練小組的帥哥們大部分都是單身。某天,他們在機房商量一個絕妙的計劃"一卡通大冒險"。這個計劃是由wf最先提出來的,計劃的內容是

簡單複習一下斯林數與

第一類斯特林數 S 1 (

深度學習 --- 隨機神經網路詳解(玻機學習演算法、執行演算法

BM網路的學習演算法 (1)  學習過程       通過有導師學習,BM網路可以對訓練集中各模式的概率分佈進行模擬,從而實現聯想記憶.學習的目的是通過調整網路權值使訓練集中的模式在網路狀態中以相同的概率再現.學習過程可分為兩個階段;第一階段

濾波演算法:卡濾波

這兩天學習了一些卡爾曼濾波演算法的相關知識。相比其它的濾波演算法,卡爾曼濾波在對計算量需求非常之低,同時又能達到相當不錯的濾波結果。 1. 演算法原理 網上看到一篇文章http://www.bzarg.com/p/how-a-kalman-filter-works-in-pictures/對

濾波個公式各個引數的意義

系統的狀態方程為: 這個狀態方程是根據上一時刻的狀態和控制變數來推測此刻的狀態,wk-1是服從高斯分佈的噪聲,是預測過程的噪聲,wk-1~N(0,Q),Q即下文的過程激勵噪聲Q. 觀測方程為:   vk是觀測的噪聲,服從高斯分佈,vk~N(0,R),R即下文的測量噪聲

十分鐘讀懂『卡濾波演算法

我是勤勞的搬運工,轉自: 1.http://blog.csdn.net/karen99/article/details/7771743 2.http://blog.csdn.net/tudouniurou/article/details/6277512 --------

機器學習學習筆記 第十葉斯演算法

貝葉斯演算法 貝葉斯要解決的問題 正向概率 逆向概率 舉例:一個班級中,男生 60%,女生 40%,男生總是穿長褲,女生則一半穿長褲一半穿裙子 正向概率:隨機選取一個學生,他(她)穿長褲的概率和穿