數(shù)據(jù)結(jié)構(gòu)與算法--最短路徑之Floyd算法
我們知道Dijkstra算法只能解決單源最短路徑問題,且要求邊上的權(quán)重都是非負(fù)的。有沒有辦法解決任意起點(diǎn)到任意頂點(diǎn)的最短路徑問題呢?如果用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();
}
其實(shí)就是有n個(gè)頂點(diǎn),創(chuàng)建了n個(gè)實(shí)例對象,每個(gè)實(shí)例傳入了不同的參數(shù)而已。我們想要一次性得到任意起點(diǎn)到任意頂點(diǎn)的最短路徑集合,可以嘗試Floyd算法。
解決多源最短路徑的Floyd算法
首先,Floyd算法可以處理負(fù)權(quán)邊,但是不能處理負(fù)權(quán)回路,也就是類似 a -> b -> c ->a,a -> b、b -> c、c -> a三條邊的權(quán)值和為負(fù)數(shù)。因?yàn)橹灰覀円恢眹@個(gè)環(huán)兜圈子,就能得到權(quán)值和任意小的路徑!負(fù)權(quán)回路會使得最短路徑的概念失去意義!
Floyd算法需要兩個(gè)二維矩陣,因此使用鄰接矩陣實(shí)現(xiàn)的有向加權(quán)圖最為方便,不過我一直用鄰接表實(shí)現(xiàn)的。為此需要將鄰接表轉(zhuǎn)換為相應(yīng)的鄰接矩陣。很簡單,先將整個(gè)二維數(shù)組用0和正無窮填充,對角線上權(quán)值為0,其余位置正無窮。然后將鄰接表中的元素覆蓋原數(shù)組中對應(yīng)位置的值,這樣鄰接表就轉(zhuǎn)換為鄰接矩陣了。鄰接矩陣在代碼中我們用dist[][]
表示,這里面存放的就是任意頂點(diǎn)到其他頂點(diǎn)的最短路徑!另外需要另外一個(gè)二維數(shù)組edge[][]
,像edge[v][w]
存放的是v到w的路徑中途經(jīng)的某一個(gè)頂點(diǎn)(或叫中轉(zhuǎn)點(diǎn)),具體來說edge[v][w]
表示v -> w這條路徑上到w的前一個(gè)頂點(diǎn)。v -> w途徑的頂點(diǎn)可能有多個(gè),都在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路徑中途徑的某一個(gè)頂點(diǎn),判斷條件其實(shí)和Dijkstra的判斷條件如出一轍,即:到底是原來v -> w的路徑比較短;還是先由v經(jīng)過k,再從k到w的這條路徑更短,如果是后者,那么需要更新相關(guān)數(shù)據(jù)結(jié)構(gòu)。Floyd依次把圖中所有頂點(diǎn)都當(dāng)做一次中轉(zhuǎn)點(diǎn),判斷任意頂點(diǎn)經(jīng)過該中轉(zhuǎn)點(diǎn)后,路徑會不會變得更短。
先放代碼...
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);
}
// 起點(diǎn)要加入
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();
}
}
}
關(guān)鍵的地方就是那三個(gè)嵌套for循環(huán)了,最外層k一定是中轉(zhuǎn)點(diǎn),第二層是路徑的起點(diǎn)v, 第三層是路徑的終點(diǎn)w, 它們是這樣的關(guān)系 v -> k -> w。v -> w途中可能有多個(gè)頂點(diǎn),k可能只是其中一個(gè)。k = 0時(shí),對所有經(jīng)過0的路徑,都更新為當(dāng)前的最短路徑,注意是當(dāng)前,也就是說是暫時(shí)的,隨著最外層k的循環(huán),dist[][]
和edge[][]
也會不斷發(fā)生變化;當(dāng)k = 1時(shí)需要用到剛k = 0更新后的dist[][]
和edge[][]
的狀態(tài),也就是說每一輪k的循環(huán)都是以上一輪為基礎(chǔ)的,到最后一次循環(huán)結(jié)束,對于經(jīng)過任意頂點(diǎn)的的所有路徑都已是最短路徑。可以看出這其實(shí)是一個(gè)動態(tài)規(guī)劃(DP)問題。
關(guān)于路徑的存放edge[][]
,有兩句代碼很關(guān)鍵
// 初始化中
edge[i][j] = i;
// if條件中
edge[v][w] = edge[k][w];
-
edge[v][w]
存放的是v -> w路徑中,終點(diǎn)w的前一個(gè)頂點(diǎn)。其實(shí)和深度優(yōu)先和廣度優(yōu)先里用到的edgeTo[]
差不多,這里的edge[][]
對于任意一條v -> w的路徑都是一個(gè)樹形結(jié)構(gòu),從終點(diǎn)w開始不斷往上找其父結(jié)點(diǎn),最后到根結(jié)點(diǎn)(即起點(diǎn)v)處停止。 -
edge[i][j] = i;
一開始初始化為起點(diǎn)i的值。意思是i -> j路徑中到j(luò)的前一個(gè)頂點(diǎn)就是i。也就是說我們先假設(shè)不經(jīng)過任何其他頂點(diǎn)的從v到w的直接路徑是最短的。在之后的循環(huán)中,如果經(jīng)過其他頂點(diǎn)的i -> j更短就更新;否則就保持默認(rèn)值。我們將看到,這樣初始化在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已經(jīng)是最短路徑了,所以只需要更新v -> w,從代碼中也可以看出來,我們確實(shí)是只對
dist[v][w]
和edge[v][w]
操作。但為什么是edge[v][w] = edge[k][w]
?現(xiàn)在v -> k -> w這條路徑更短,k -> w中到w的前一個(gè)頂點(diǎn)也就是v -> w路徑中到w的前一個(gè)頂點(diǎn)。結(jié)合edge[v][w]
的定義:存放的是v -> w路徑中,w的前一個(gè)頂點(diǎn),可得到edge[v][w] = edge[k][w]
。畫個(gè)圖加深理解。
下圖是v -> w第一次更新時(shí):k - > w中到w的前一個(gè)頂點(diǎn)應(yīng)該是k,同時(shí)它也是v -> w路徑中到w的前一個(gè)頂點(diǎn)。所以edge[k][w]
應(yīng)該為k。而事實(shí)確實(shí)是這樣的!因?yàn)樵诔跏蓟瘯r(shí)候我們是這樣做的edge[i][j] = i
。
edge[v][w] = edge[k][w] = k
,這里其實(shí)就是用了初始值而已。
再看下圖,是若干次更新v -> w時(shí),此時(shí)v -> k和k -> w路徑中可能有多個(gè)頂點(diǎn),但是edge[k][w]
存的始終是終點(diǎn)w的前一個(gè)頂點(diǎn)。當(dāng)v -> w的最短路徑更新后,k -> w中到w的前一個(gè)頂點(diǎn)就是v -> w路徑中到w的前一個(gè)頂點(diǎn)。
這就解釋了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]
by @sunhaiyu
2017.9.24