1. 程式人生 > >資料結構與演算法--最短路徑之Floyd演算法

資料結構與演算法--最短路徑之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]