數(shù)據(jù)結(jié)構(gòu)與算法--最短路徑之Floyd算法

數(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,249評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內(nèi)容