我在上一篇博客中講解了狄克斯特拉算法,該算法可以用於尋找權值都爲正的有向無環圖的最短路徑,我也提到了如果碰到權值爲負的情況可以使用貝爾曼-福德算法,那麼今天就讓我們學習一下貝爾曼-福德算法是如何處理負權值和環路的。
其實理解了狄克斯特拉算法之後理解貝爾曼-福德算法就很容易了,如果說狄克斯特拉算法是用一種“廣度”的方式,每次尋找源點所能到達的距離最短的一個點,更新其鄰居節點的距離。那麼貝爾曼-福德算法就是用一種“深度”的方式,每次找到所有的路徑,進行“鬆弛”操作最終找到最短路徑。貝爾曼-福德算法每次對所有的邊進行鬆弛,每次鬆弛都會得到一條最短路徑,所以總共需要要做的鬆弛操作是V - 1次。在完成這麼多次鬆弛後如果還是可以鬆弛的話,那麼就意味着,其中包含負環。相比狄克斯特拉算法其時間複雜度較高,達到O(V*E),V代表頂點數,E代表邊數。
你可能不瞭解什麼是鬆弛操作,其實就是不斷尋找距離更短的路徑:
假設有這樣的一個圖
從原點出發經由一條路徑直達A的路徑爲M,直達B的路徑爲N,存在從A到B的路徑L,如果你找到M+L<N的路徑了,那麼就是做了一次鬆弛操作
那麼爲什麼說要做V-1次鬆弛操作呢?
考慮無環圖的極限情況,所有節點排成一條線,那麼最遠的路段是V-1
那麼遇到有環圖呢?
最長路徑便成了無限,但沒經過一次環至少增加兩短路,我們只要經過V-1次鬆弛便可以檢測出來是否有環,對於有環的圖我們還要對其性質進行判斷,如果是正權值的環那麼就排除改路徑,如果是負權值的環那麼此題就無法求解,因爲走環的次數越多權值和越小。
如果你瞭解狄克斯特拉算法的話寫這個也很容易,我們同樣需要三個表一個距離表一個父節點表一個全部節點表,然後就是遍歷V-1次,並判斷是否存在環
我們找一個圖 起始的樣子是這樣的
我們得到了一張從Start開始,第一步只能到達A、B兩點的初始化的距離表,也就是說,此時到達A、B的距離是已知的,因爲可以由Start直接到達; 得到一個只有Start點作爲父節點的兩個點A、B的父地點表;還有一個全部路徑的路徑圖。
我們遍歷路徑表中的 相鄰兩點之間的全部路徑 一次,根據距離表更新每個點的到達距離。也就是對圖進行一次鬆弛操作:
我們可以看到,初始化的時候,Start確定的是隻能直接到達A、B兩點。我們遍歷所有的 兩點之間 的路徑(比如A、C之間的這條 A => C路徑,起點爲:A;終點爲:C;權值爲:2)。我們根據距離表,知道了起點到A的距離是5,到B的距離是3。由於A能到達C、D(存在路徑:A => C 、A => D),所以,此時通過A到達C、D的距離就是(B點同理):
對於經過A直接到達的點:
C的距離 = 5(Start到A的距離)+ 2(A到C的距離,也就是 A => C 的權值)= 7
D的距離 = 5(Start到A的距離)+ 2(A到C的距離,也就是 A => C 的權值)= 7
對於經過B直接到達的點:
C的距離 = 3 (Start到B的距離)+ 1(B到C的距離,也就是 B=> C 的權值)= 4
E的距離 = 3 (Start到B的距離)+ 6(B到E的距離,也就是 B=> C 的權值)= 9
我們依次遍歷所有相鄰兩點之間的路徑,遍歷到 A => C 的時候,我們得到Start到C的距離是 5 + 2 = 7,將其寫入距離表中;但是之後遍歷到 B => C 的時候,我們又得到了Start到C的距離是 3 + 1 = 4 < 7,所以我們更改距離表,到達C的距離就更新爲了4遍歷完所有路徑後(鬆弛之後),我們更新了圖中的距離表,就是現在這個樣子。同樣,我們將記錄進距離表的點的相應的父節點也寫進父地點表。
我們可以對比第一張圖來看,此時我們做的操作,其實就是基於深度(淺色標線)的一次鬆弛操作。
我們再一次遍歷全部的相鄰兩點之間的路徑,我們可以得到如下三張圖:
同樣的操作,我們繼續遍歷所有相鄰兩點之間的路徑(包括之前已經記錄過距離的點,每次都遍歷全部,因爲不知道之前遍歷過的會不會被再次更新,再次鬆弛)。所以這次我們鬆弛的邊分別是(這個圖來說之前兩部遍歷過後,有一些邊再鬆弛權重已經不會改變):
D => C、D => End,C => End、E => End
D => C : 查表得知到D的距離是7,D到C的距離是3,7 + 3 = 10 > 4(表中C的距離),所以C的距離不更新;
D => End: 查表知到D的距離是7,D到End的距離是4,7 + 4 = 11,表中無End,所以加入End = 11, 父地點爲D;
C => End: 查表知到C的距離是4,C到End的距離是5,4 + 5 = 9 < 11(D=>End寫入的),所以更新End的距離爲 9, 父節點爲C;
E => End: 查表知到E的距離是9, E到End的距離是1,9 + 1 = 10 > 9 (C => End更新過的),所以不更新距離表和父節點表。
到此更新完畢了。我們得到了最終的距離表和父地點表。這樣就得到了結論,從Start到到End的最小距離是9,路徑是:End => C(End的父節點) => B(C的父節點) => Start(B的父節點) 的反轉 Start => B => C => End。
求解新問題
具體代碼:
package classic;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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);
}
}
}
}
/**
* 代表"一條邊"的信息對象
*/
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;
}
}
}