有權最短路徑問題:貝文曼福德(Bellman Ford)演算法 & Java 實現
阿新 • • 發佈:2019-01-24
一、貝爾曼福德演算法
1. 簡介
貝爾曼福德(Bellman Ford)演算法也是求解單源最短路徑問題,相比狄克斯特拉(dijkstra)演算法,它執行效率會差一些,但是它可以處理邊的權重為負值的情況,而狄克斯特拉演算法要求變的權重不能為負數。
2. 演算法思想
鬆弛操作指標對邊(u,v),如果 v.distance > u.distance + weight,則更新 v.distance 的值。
貝爾曼福德演算法通過對邊進行鬆弛操作來漸近地降低每個頂點到起始點的距離。
當存在權重為負值的環路時,演算法返回 False
,表示不存在解決方案(因為我們可以在權重為負的環路上無限迴圈,使得距離變成負無窮大)。
當不存在權重為負值的環路時,演算法返回 True
3. 圖解過程
圖中是我們對每條邊進行鬆弛操作的過程,陰影邊表示前驅值。我們可以證明當不存在權重為負數的環路時,對每條邊迴圈執行 V - 1 次鬆弛操作後,每個頂點的距離將就是最短距離(證明過程這裡不給了,在《演算法導論》相關章節中有,我們記住結論就行)。
二、程式碼實現
1. Edge 類
由於該演算法中需要經常對邊進行操作,所以我們還是把邊寫為一個具體的類,方便操作。
public class Edge {
private Vertex start;
private Vertex end;
private int weight; // 權重(邊的長度)
public Edge(Vertex start, Vertex end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
public Vertex getStart() {
return start;
}
public Vertex getEnd() {
return end;
}
public int getWeight () {
return weight;
}
}
2. Vertex 類
這裡我們使用 Edge 類的一個集合來表示該頂點可以到達的鄰居頂點。
import java.util.LinkedList;
public class Vertex {
private char id; // 頂點的標識
private LinkedList<Edge> edges; // 當前頂點可直接達到的邊
private Vertex parent; // 上一個頂點是誰(前驅),用來記錄路徑的
private int distance = Integer.MAX_VALUE; // 距離起始點的距離
public Vertex(char id) {
this.id = id;
this.edges = new LinkedList<>();
}
public char getId() {
return id;
}
public LinkedList<Edge> getNeighbors() {
return edges;
}
public void addNeighbor(Vertex vertex, int weight) {
edges.add(new Edge(this, vertex, weight)); // 起點均是當前頂點
}
public Vertex getParent() {
return parent;
}
public void setParent(Vertex parent) {
this.parent = parent;
}
public int getDistance() {
return distance;
}
public void setDistance(int distance) {
this.distance = distance;
}
@Override
public String toString() {
return String.format("Vertex[%c]: distance is %d , predecessor is '%s'", id, distance, parent == null ? "null"
: parent.id);
}
}
3. 場景類
說明看程式碼註釋吧,註釋應該寫的比較清楚了。
import java.util.LinkedList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Vertex> list = getTestData();
// 設定第0個頂點為起始點
Vertex source = list.get(0);
source.setDistance(0);
// 貝文曼福德演算法
boolean flag = bellmanFord(list, source);
// 列印結果
System.out.println("是否存在解決方案:" + flag);
for (int i = 0; i < list.size(); i++) {
Vertex vertex = list.get(i);
System.out.println(vertex.toString());
}
}
public static boolean bellmanFord(List<Vertex> list, Vertex source) {
// 1. 將所有邊新增到一個佇列中
LinkedList<Edge> queue = new LinkedList<>();
for (int i = 0; i < list.size(); i++) {
queue.addAll(list.get(i).getNeighbors());
}
// 2. 需要執行 (V-1)*E 次鬆弛操作
for (int i = 1; i < list.size(); i++) {
for (Edge edge : queue) {
relax(edge);
}
}
// 3. 驗證是否存在權重為負數的環路
for (Edge edge : queue) {
Vertex u = edge.getStart();
Vertex v = edge.getEnd();
// 對於邊 (u, v),如果 v.distance > u.distance + weight,則說明是負數環路
if (v.getDistance() > u.getDistance() + edge.getWeight()) {
return false;
}
}
return true;
}
public static void relax(Edge edge) {
Vertex start = edge.getStart();
Vertex end = edge.getEnd();
int distance = start.getDistance() + edge.getWeight();
if (end.getDistance() > distance) {
end.setDistance(distance);
end.setParent(start);
}
}
public static List<Vertex> getTestData() {
Vertex s = new Vertex('s');
Vertex t = new Vertex('t');
Vertex x = new Vertex('x');
Vertex y = new Vertex('y');
Vertex z = new Vertex('z');
s.addNeighbor(t, 6); // s->t : 6
s.addNeighbor(y, 7); // s->y : 7
t.addNeighbor(x, 5); // t->x : 5
t.addNeighbor(y, 8); // t->y : 8
t.addNeighbor(z, -4); // t->z : -4
x.addNeighbor(t, -2); // x->t : -2
y.addNeighbor(x, -3); // y->x : -3
y.addNeighbor(z, 9); // y->z : 9
z.addNeighbor(s, 2); // z->s : 2
z.addNeighbor(x, 7); // z->x : 7
LinkedList<Vertex> list = new LinkedList<>();
list.add(s);
list.add(t);
list.add(x);
list.add(y);
list.add(z);
return list;
}
}
4. 執行結果
是否存在解決方案:true
Vertex[s]: distance is 0 , predecessor is 'null'
Vertex[t]: distance is 2 , predecessor is 'x'
Vertex[x]: distance is 4 , predecessor is 'y'
Vertex[y]: distance is 7 , predecessor is 's'
Vertex[z]: distance is -2 , predecessor is 't'
對應下圖,由於本例中沒有權重為負數的環路,所有存在解決方案,且每個頂點的最短距離結果也正確。