概述
Bellman Ford算法可以用來解決加權圖中的最短路徑問題。其與Dijkstra算法的區別在於Belllman Ford算法的應用範圍更廣,例如其可以用來處理帶有負權重的加權圖中的最短路徑問題。由於Dijkstra算法本質上是一種貪心算法,因而當圖中存在路徑權值之和爲負的環時,Dijkstra算法會給出錯誤的結果因爲其總是偏向於選擇當前情況下的局部最優路徑。Bellman Ford算法的時間複雜度高於Dijkstra算法。
歷史
Bellman Ford算法最初於1955年被Alfonso Shimbel提出,但最終基於1958和1956年發表論文的 Richard Bellman和Lester Ford, Jr.二人命名。Edward F. Moore於1957年也提出了該算法,因此該算法有時也稱作是Bellman Ford Moore算法。
負權重帶環圖的問題
包含負權重的圖在不少實際問題中有所應用。例如在尋找化學反應鏈中需要最少能量的路徑中,該反應鏈可能既包含吸熱反應也包含放熱反應,那麼我們可以認爲放熱反應是負權重路徑而吸熱反應是正權重路徑。在包含負權重帶環圖的問題中,從起始點開始的一條路徑如果能夠到達一個整體權重之和爲負的環,則最短路徑在中情況下將不存在,因爲這一路徑總可以通過在負權重路徑中循環來不斷降低總權重。而使用Bellman Ford算法則能夠發現這一問題。
算法描述
簡單來說,類似於Dijkstra算法,Bellman Ford算法首先將從源結點到達剩餘所有結點的距離初始化爲無窮大,而從源結點到其本身的距離則初始化爲0。然後算法將循環檢查圖中的每一條邊。如果這條邊能夠縮短從源結點到某一目的結點的距離,則新的最短距離將被記錄下來。具體來說,對於一條邊u -> v
,設其權重爲weight(u, v)
,而結點u
,v
距離源結點src
的距離分別爲dist[u]
,dist[v]
。則dist[v] = min(dist[v], dist[u] + weight(u,v)
。在每一次循環中該算法都檢查所有的邊。對於第i
次循環,算法將得到從源結點出發不超過i
步能夠到達的結點的最短距離(在某些情況下也可能多於i
)。因爲對於N
個結點的圖來說,不包含環的最短距離最長爲N-1
,所以該算法需要循環檢查所有邊N-1
次。
在循環結束後,算法將再多循環檢查一遍所有的邊並嘗試更新最短路徑。如果從源結點出發到某一點的最短距離在這一次循環中能夠被更新,則說明在這一路徑上至少存在一個權重之和爲負的環。
算法的Java實現
public class BellmanFord {
/**
* Find the shortest path from src to all other nodes in the graph
* represented using an array of arrays of shape [u, v, weight],
* in which u and v are the start and end point of the edge, respectively.
* @param n Number of nodes in the graph.
* @return An array containing the shortest path from src to all other
* nodes in the graph, with the predecessor of the nodes. For each node i, the
* array contains a two-tuple [distance, predecessor].
* If a shortest path from src to node does not exist, distance and predecessor will
* both be Integer.MAX_VALUE.
*/
public int[][] findShortestPath(int src, int n, int[][] edges) {
// first step, initialize the distance
int[] dist = new int[n];
int[] pre = new int[n];
int INF = Integer.MAX_VALUE / 2;
Arrays.fill(dist, INF);
dist[src] = 0;
Arrays.fill(pre, Integer.MAX_VALUE);
// second step, compute shortest path with at most n-1 steps
for (int i = 1; i < n; i++) {
for (int[] edge : edges) {
int u = edge[0], v = edge[1], weight = edge[2];
if (dist[v] > dist[u] + weight) {
pre[v] = u;
}
dist[v] = Math.min(dist[v], dist[u] + weight);
}
}
// third step, check the existance of negative weight cycle
for (int[] edge : edges) {
int u = edge[0], v = edge[1], weight = edge[2];
if (dist[u] < INF && dist[u] + weight < dist[v]) {
System.out.println("Graph contains negative weight cycle");
}
}
int[][] ret = new int[n][2];
for (int i = 0; i < n; i++) {
ret[i] = new int[]{dist[i] == INF ? Integer.MAX_VALUE : dist[i], pre[i]};
}
return ret;
}
}
時間複雜度
從上面代碼中我們可以看出我們在每一次循環中都會檢查所有的邊,共計循環了N次,那麼算法的時間複雜度爲O(N * E)
,其中N
爲圖中的結點總數,E
爲圖中的邊的數目。
空間複雜度
O(N)
。因爲我們使用了O(N)
大小的數組來存儲最短路徑的距離。
潛在優化
對於step 2,如果在某一次循環中我們沒有更新任何最短距離,則這一循環可以提前結束。因爲這證明該算法已經找到了從源結點出發能夠到達的所有結點的最短路徑。這一優化使得step 2的循環次數能夠少於O(N-1)
。