資料結構與演算法--最短路徑之Floyd演算法
一、解決單源最短路徑問題的Dijkstra演算法
我們知道Dijkstra演算法只能解決單源最短路徑問題,且要求邊上的權重都是非負的。那麼有沒有辦法解決任意起點到任意頂點的最短路徑問題呢?如果用Dijkstra演算法,可以這樣做:
Dijkstra[] all = new Dijkstra[graph.vertexNum()]; for (int i = 0; i < all.length; i++) { all[i] = new Dijkstra(graph, i); } for (int s = 0; s < all.length; s++) { for (int i = 0; i < graph.vertexNum(); i++) { System.out.print(s + " to " + i + ": "); System.out.print("(" + all[s].distTo(i) + ") "); System.out.println(all[s].pathTo(i)); } System.out.println(); }
其實就是有n個頂點,建立了n個例項物件,每個例項傳入了不同的引數而已。我們想要一次性得到任意起點到任意頂點的最短路徑集合,可以嘗試Floyd演算法。
二、解決多源最短路徑問題的Floyd演算法
首先,Floyd演算法可以處理負權邊,但是不能處理負權迴路,也就是類似 a -> b -> c ->a,a -> b、b -> c、c -> a三條邊的權值和為負數。因為只要我們一直圍著這個環兜圈子,就能得到權值和任意小的路徑!負權迴路會使得最短路徑的概念失去意義!
Floyd演算法需要兩個二維矩陣,因此使用鄰接矩陣實現的有向加權圖最為方便,不過我一直用鄰接表實現的。為此需要將鄰接錶轉換為相應的鄰接矩陣。很簡單,先將整個二維陣列用0和正無窮填充,對角線上權值為0,其餘位置正無窮。然後將鄰接表中的元素覆蓋原陣列中對應位置的值,這樣鄰接表就轉換為鄰接矩陣了。鄰接矩陣在程式碼中我們用dist[][]
edge[][]
,像edge[v][w]
存放的是v到w的路徑中途經的某一個頂點(或叫中轉點),具體來說edge[v][w]
表示v -> w這條路徑上到w的前一個頂點。v -> w途徑的頂點可能有多個,都在v那一行即edge[v][i]
裡找。演算法的精華在下面幾行:
if (dist[v][k] + dist[k][w] < dist[v][w]) {
dist[v][w] = dist[v][k] + dist[k][w];
edge[v][w] = edge[k][w];
}
其中k是v -> w路徑中途徑的某一個頂點,判斷條件其實和Dijkstra的判斷條件如出一轍,即:到底是原來v -> w的路徑比較短;還是先由v經過k,再從k到w的這條路徑更短,如果是後者,那麼需要更新相關資料結構。Floyd依次把圖中所有頂點都當做一次中轉點,判斷任意頂點經過該中轉點後,路徑會不會變得更短。
package Chap7;
import java.util.LinkedList;
import java.util.List;
public class Floyd {
private double[][] dist;
private int[][] edge;
public Floyd(EdgeWeightedDiGraph<?> graph) {
dist = new double[graph.vertexNum()][graph.vertexNum()];
edge = new int[graph.vertexNum()][graph.vertexNum()];
// 將鄰接表變成了鄰接矩陣
for (int i = 0; i < dist.length; i++) {
for (int j = 0; j < dist.length; j++) {
// 賦值給
edge[i][j] = i;
if (i == j) {
dist[i][j] = 0.0;
} else {
dist[i][j] = Double.POSITIVE_INFINITY;
}
}
}
for (int v = 0; v < graph.vertexNum(); v++) {
for (DiEdge edge : graph.adj(v)) {
int w = edge.to();
dist[v][w] = edge.weight();
}
}
for (int k = 0; k < graph.vertexNum(); k++) {
for (int v = 0; v < dist.length; v++) {
for (int w = 0; w < dist.length; w++) {
if (dist[v][k] + dist[k][w] < dist[v][w]) {
dist[v][w] = dist[v][k] + dist[k][w];
edge[v][w] = edge[k][w];
}
}
}
}
}
public boolean hasPathTo(int s, int v) {
return dist[s][v] != Double.POSITIVE_INFINITY;
}
public Iterable<Integer> pathTo(int s, int v) {
if (hasPathTo(s, v)) {
LinkedList<Integer> path = new LinkedList<>();
for (int i = v; i != s; i = edge[s][i]) {
path.push(i);
}
// 起點要加入
path.push(s);
return path;
}
return null;
}
public double distTo(int s, int w) {
return dist[s][w];
}
public static void main(String[] args) {
List<String> vertexInfo = List.of("v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7");
int[][] edges = {{4, 5}, {5, 4}, {4, 7}, {5, 7}, {7, 5}, {5, 1}, {0, 4}, {0, 2},
{7, 3}, {1, 3}, {2, 7}, {6, 2}, {3, 6}, {6, 0}, {6, 4}};
double[] weight = {0.35, 0.35, 0.37, 0.28, 0.28, 0.32, 0.38, 0.26, 0.39, 0.29,
0.34, 0.40, 0.52, 0.58, 0.93};
EdgeWeightedDiGraph<String> graph = new EdgeWeightedDiGraph<>(vertexInfo, edges, weight);
Floyd floyd = new Floyd(graph);
for (int s = 0; s < graph.vertexNum(); s++) {
for (int w = 0; w < graph.vertexNum(); w++) {
System.out.print(s + " to " + w + ": ");
System.out.print("(" + floyd.distTo(s, w) + ") ");
System.out.println(floyd.pathTo(s, w));
}
System.out.println();
}
}
}
關鍵的地方就是那三個巢狀for迴圈了,最外層k一定是中轉點,第二層是路徑的起點v, 第三層是路徑的終點w, 它們是這樣的關係 v -> k -> w。v -> w途中可能有多個頂點,k可能只是其中一個。k = 0時,對所有經過0的路徑,都更新為當前的最短路徑,注意是當前,也就是說是暫時的,隨著最外層k的迴圈,dist[][]
和edge[][]
也會不斷髮生變化;當k = 1時需要用到剛k = 0更新後的dist[][]
和edge[][]
的狀態,也就是說每一輪k的迴圈都是以上一輪為基礎的,到最後一次迴圈結束,對於經過任意頂點的的所有路徑都已是最短路徑。可以看出這其實是一個動態規劃(DP)問題。
關於路徑的存放edge[][]
,有兩句程式碼很關鍵:
// 初始化中
edge[i][j] = i;
// if條件中
edge[v][w] = edge[k][w];
edge[v][w]
存放的是v -> w路徑中,終點w的前一個頂點。其實和深度優先和廣度優先裡用到的edgeTo[]
差不多,這裡的edge[][]
對於任意一條v -> w的路徑都是一個樹形結構,從終點w開始不斷往上找其父結點,最後到根結點(即起點v)處停止。edge[i][j] = i;
一開始初始化為起點i的值。意思是i -> j路徑中到j的前一個頂點就是i。也就是說我們先假設不經過任何其他頂點的從v到w的直接路徑是最短的。在之後的迴圈中,如果經過其他頂點的i -> j更短就更新;否則就保持預設值。我們將看到,這樣初始化在edge[v][w] = edge[k][w]
這句中也適用。
[0, 0, 0, 0, 0, 0, 0, 0]
[1, 1, 1, 1, 1, 1, 1, 1]
[2, 2, 2, 2, 2, 2, 2, 2]
[3, 3, 3, 3, 3, 3, 3, 3]
[4, 4, 4, 4, 4, 4, 4, 4]
[5, 5, 5, 5, 5, 5, 5, 5]
[6, 6, 6, 6, 6, 6, 6, 6]
[7, 7, 7, 7, 7, 7, 7, 7]
- 我們知道v -> k -> w的路徑中,v -> k已經是最短路徑了,所以只需要更新v -> w,從程式碼中也可以看出來,我們確實是只對
dist[v][w]
和edge[v][w]
操作。但為什麼是edge[v][w] = edge[k][w]
?現在v -> k -> w這條路徑更短,k -> w中到w的前一個頂點也就是v -> w路徑中到w的前一個頂點。結合edge[v][w]
的定義:存放的是v -> w路徑中,w的前一個頂點,可得到edge[v][w] = edge[k][w]
。畫個圖加深理解。
下圖是v -> w第一次更新時:k - > w中到w的前一個頂點應該是k,同時它也是v -> w路徑中到w的前一個頂點。所以edge[k][w]
應該為k。而事實確實是這樣的!因為在初始化時候我們是這樣做的edge[i][j] = i
。
edge[v][w] = edge[k][w] = k
,這裡其實就是用了初始值而已。
再看下圖,是若干次更新v -> w時,此時v -> k和k -> w路徑中可能有多個頂點,但是edge[k][w]
存的始終是終點w的前一個頂點。當v -> w的最短路徑更新後,k -> w中到w的前一個頂點就是v -> w路徑中到w的前一個頂點。
這就解釋了edge[v][w] = edge[k][w]
是怎麼來的。
最後得到的edge[][]
如下:
[0, 5, 0, 7, 0, 4, 3, 2]
[6, 1, 6, 1, 6, 7, 3, 2]
[6, 5, 2, 7, 5, 7, 3, 2]
[6, 5, 6, 3, 6, 7, 3, 2]
[6, 5, 6, 7, 4, 4, 3, 4]
[6, 5, 6, 1, 5, 5, 3, 5]
[6, 5, 6, 7, 6, 7, 6, 2]
[6, 5, 6, 7, 5, 7, 3, 7]