演算法簡介
貝爾曼-福特演算法(Bellman–Ford algorithm )用於計算出起點到各個節點的最短距離,支援存在負權重的情況
-
它的原理是對圖進行最多V-1次鬆弛操作,得到所有可能的最短路徑。其優於迪科斯徹演算法的方面是邊的權值可以為負數、實現簡單,缺點是時間複雜度過高,高達O(VE)。但演算法可以進行若干種優化,提高了效率。
-
Bellman Ford演算法每次對所有的邊進行鬆弛,每次鬆弛都會得到一條最短路徑,所以總共需要要做的鬆弛操作是V - 1次。在完成這麼多次鬆弛後如果還是可以鬆弛的話,那麼就意味著,其中包含負環。
-
相比狄克斯特拉演算法(Dijkstra algorithm),其最大優點便是Bellman–Ford支援存在負權重的情況,並且程式碼實現相對簡單。缺點便是時間複雜度較高,達到O(V*E),V代表頂點數,E代表邊數。
可用於解決以下問題:
- 從A出發是否存在到達各個節點的路徑(有計算出值當然就可以到達);
- 從A出發到達各個節點最短路徑(時間最少、或者路徑最少等)
- 圖中是否存在負環路(權重之和為負數)
其思路為:
-
初始化時將起點s到各個頂點v的距離dist(s->v)賦值為∞,dist(s->s)賦值為0
-
後續進行最多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
-
遍歷都結束後,若再進行一次遍歷,還能得到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
複製程式碼