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

CodeInfo發表於2019-03-04

演算法簡介

貝爾曼-福特演算法(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("
圖中存在負環路,無法求解
");
				hasRing = true;
				break;
			}
		}
		
		if(!hasRing) {
			//列印出到各個節點的最短路徑
			for(String key : costMap.keySet()) {
				System.out.print("
到目標節點"+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
複製程式碼

微信公眾號

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

相關文章