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

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

加權(quán)圖中,我們很可能關(guān)心這樣一個(gè)問題:從一個(gè)頂點(diǎn)到另一個(gè)頂點(diǎn)成本最小的路徑。比如從成都到北京,途中還有好多城市,如何規(guī)劃路線,能使總路程最小;或者我們看重的是路費(fèi),那么如何選擇經(jīng)過的城市可以使得總路費(fèi)降到最低?

  • 首先路徑是有向的,最短路徑需要考慮到各條邊的方向。
  • 權(quán)值不一定就是指距離,還可以是費(fèi)用等等...

最短路徑的定義:在一幅有向加權(quán)圖中,從頂點(diǎn)s到頂點(diǎn)t的最短路徑是所有從s到t的路徑中權(quán)值最小者。

為此,我們先要定義有向邊以及有向圖。

加權(quán)有向圖的實(shí)現(xiàn)

首先是有向邊。

package Chap7;

public class DiEdge {
    private int from;
    private int to;
    private double weight;

    public DiEdge(int from, int to, double weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }

    public int from() {
        return from;
    }

    public int to() {
        return to;
    }

    public double weight() {
        return weight;
    }
    
    @Override
    public String toString() {
        return "(" +
                from +
                "->" + to +
                " " + weight +
                ')';
    }
}

比起無向邊Edge類,更簡(jiǎn)單些,因?yàn)閮蓚€(gè)頂點(diǎn)有明顯的先后順序。

然后是加權(quán)有向圖。

package Chap7;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class EdgeWeightedDiGraph<Item> {
    private int vertexNum;
    private int edgeNum;
    // 鄰接表
    private List<List<DiEdge>> adj;
    // 頂點(diǎn)信息
    private List<Item> vertexInfo;

    public EdgeWeightedDiGraph(List<Item> vertexInfo) {
        this.vertexInfo = vertexInfo;
        this.vertexNum = vertexInfo.size();
        adj = new ArrayList<>();
        for (int i = 0; i < vertexNum; i++) {
            adj.add(new LinkedList<>());
        }
    }


    public EdgeWeightedDiGraph(List<Item> vertexInfo, int[][] edges, double[] weight) {
        this(vertexInfo);
        for (int i = 0; i < edges.length; i++) {
            DiEdge edge = new DiEdge(edges[i][0], edges[i][1], weight[i]);
            addDiEdge(edge);
        }
    }

    public EdgeWeightedDiGraph(int vertexNum) {
        this.vertexNum = vertexNum;
        adj = new ArrayList<>();
        for (int i = 0; i < vertexNum; i++) {
            adj.add(new LinkedList<>());
        }
    }

    public EdgeWeightedDiGraph(int vertexNum, int[][] edges, double[] weight) {
        this(vertexNum);
        for (int i = 0; i < edges.length; i++) {
            DiEdge edge = new DiEdge(edges[i][0], edges[i][1], weight[i]);
            addDiEdge(edge);
        }
    }

    public void addDiEdge(DiEdge edge) {
        adj.get(edge.from()).add(edge);
        edgeNum++;
    }

    // 返回與某個(gè)頂點(diǎn)依附的所有邊
    public Iterable<DiEdge> adj(int v) {
        return adj.get(v);
    }

    public List<DiEdge> edges() {
        List<DiEdge> edges = new LinkedList<>();
        for (int i = 0; i < vertexNum; i++) {
            for (DiEdge e : adj(i)) {
                edges.add(e);
            }
        }
        return edges;
    }

    public int vertexNum() {
        return vertexNum;
    }

    public int edgeNum() {
        return edgeNum;
    }

    public Item getVertexInfo(int i) {
        return vertexInfo.get(i);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(vertexNum).append("個(gè)頂點(diǎn), ").append(edgeNum).append("條邊。\n");
        for (int i = 0; i < vertexNum; i++) {
            sb.append(i).append(": ").append(adj.get(i)).append("\n");
        }
        return sb.toString();
    }

    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);

        System.out.println("該圖的鄰接表為\n"+graph);
        System.out.println("該圖的所有邊:"+ graph.edges());

    }
}

實(shí)現(xiàn)和加權(quán)無向圖差不多,就改了addEdgeadj方法。addEdge由于邊有向,不會(huì)對(duì)稱地存儲(chǔ)邊;adj方法不像無向圖那樣鄰接表中有重復(fù)的邊,有向圖中鄰接表中的邊都是唯一的,所以全部加入即可。

最短路徑的數(shù)據(jù)結(jié)構(gòu)

1、最短路徑樹中的邊

和深度優(yōu)先、廣度優(yōu)先搜索一樣,我們將用到一個(gè)edgeTo[]表示一個(gè)樹形結(jié)構(gòu),edgeTo[v]表示樹中連接頂點(diǎn)v和其父結(jié)點(diǎn)的邊(也就是起點(diǎn)s到v的路徑上最后一條邊)。

2、起點(diǎn)到各個(gè)頂點(diǎn)的最短距離

和Prim算法類似,需要一個(gè)distTo[]。Prim算法中它存放的是:到某個(gè)頂點(diǎn)權(quán)值最小的那條邊。而最短路徑中,distTo[v]存放的是:從起點(diǎn)s開始到某頂點(diǎn)v的最短路徑長(zhǎng)度。我們約定到起點(diǎn)s的最短路徑長(zhǎng)度為0,即distTo[s] = 0;同時(shí)約定從起點(diǎn)s到不可達(dá)的頂點(diǎn)的距離均為正無窮

最短路徑算法的基礎(chǔ)基于一個(gè)被稱為松弛的簡(jiǎn)單操作。放松一條邊v -> w意味著檢查s到w的最短路徑是否是 先從s到v,再?gòu)膙到w。如果是就更新相關(guān)數(shù)據(jù)結(jié)構(gòu)的內(nèi)容;如果不是,不作更改。用代碼可以表示為

// v -> w, v和w是邊edge的兩個(gè)頂點(diǎn)
// distTo[v] :s到v的最短距離;distTo[w]:s到w的最短距離
if (distTo[v] + edge.weight() < distTo[w]) {
    distTo[w] = distTo[v] + e.weight();
    edgeTo[w] = edge;
}

再用一幅圖加深理解。

先看左邊兩個(gè)圖:s到v的最短距離是3.1,s到w的最短距離是3.3。當(dāng)在頂點(diǎn)v時(shí),檢查它的鄰接點(diǎn)w,邊v -> w的權(quán)值是1.3,從s到w的當(dāng)然不能先從s到v,再?gòu)膙到w,因?yàn)檫@倆加起來都4.4,比原來s到w的方案還要費(fèi)勁,所以不會(huì)更改distTo[w]edgeTo[w]。此時(shí)我們說v -> w這條邊失效并忽略它。

再看右邊兩個(gè)圖:原先s到w的方案距離為7.2,現(xiàn)在我們換條路走,從s先到v,再?gòu)膙到w,只有4.4!這是條到w更近的路。所以更新,distTo[w]改成4.4,到s到w的最后一條邊edgeTo[w]也改成了v- > w這條邊。此時(shí)就稱邊v -> w放松成功(可以想象成一根緊繃的橡皮筋,它的長(zhǎng)度比較長(zhǎng);橡皮筋放松后,長(zhǎng)度變短。)

對(duì)頂點(diǎn)的放松就是:放松由該頂點(diǎn)引出的所有邊

在實(shí)現(xiàn)之前,對(duì)于最短路徑算法我們需要了解得更多,來看幾個(gè)命題。

  • 當(dāng)且僅當(dāng)對(duì)于從v -> w的任意一條邊,都有dist[w] <= distTo[v] + edge.weight(),那么s到w的路徑都是最短路徑。
  • Dijkstra算法能解決邊權(quán)值非負(fù)的加權(quán)有向圖的單點(diǎn)最短路徑問題,換句話說,當(dāng)遇到有負(fù)權(quán)值的邊,或者想通過一次運(yùn)算就找到任意頂點(diǎn)到任意頂點(diǎn)的最短路徑,Dijkstra就不適用了。
  • 如果v是從起點(diǎn)s可達(dá)的,那么邊v -> w只會(huì)被放松一次,放松v時(shí),必有dist[w] <= distTo[v] + edge.weight(),該等式在算法整個(gè)流程都成立,所以distTo[w]只能減小。而distTo[v]不會(huì)改變,因?yàn)槊看味歼x擇distTo[]最小的頂點(diǎn),之后的放松操作不可能使得任何distTo[]的值小于dist[v]。也就是說,每次選擇distTo[]最小的頂點(diǎn),它的值不會(huì)小于那些已經(jīng)放松過的頂點(diǎn)的最短路徑值distTo[v],也不會(huì)大于任意未被放松過的頂點(diǎn)。所有從s可達(dá)的頂點(diǎn)都會(huì)按照distTo[]里最短路徑的權(quán)值來依次放松。
  • 最短路徑算法也可以處理無向圖,用有向圖的數(shù)據(jù)類型,只是對(duì)應(yīng)于無向圖,每條邊都會(huì)創(chuàng)建兩條方向不同的有向邊。例如,無向圖中的邊3-0,使用有向圖創(chuàng)建3 -> 0和0 -> 3兩條邊,然后調(diào)用最短路徑算法即可。

Dijkstra算法的實(shí)現(xiàn)

package Chap7;

import java.util.*;

public class Dijkstra {
    private DiEdge[] edgeTo;
    private double[] distTo;
    private Map<Integer, Double> minDist;

    public Dijkstra(EdgeWeightedDiGraph<?> graph, int s) {
        edgeTo = new DiEdge[graph.vertexNum()];
        distTo = new double[graph.vertexNum()];
        minDist = new HashMap<>();

        for (int i = 0; i < graph.vertexNum(); i++) {
            distTo[i] = Double.POSITIVE_INFINITY; // 1.0 / 0.0為INFINITY
        }
        // 到起點(diǎn)距離為0
        distTo[s] = 0.0;
        relax(graph, s);
        while (!minDist.isEmpty()) {
            relax(graph, delMin());
        }
    }

    private int delMin() {
        Set<Map.Entry<Integer, Double>> entries = minDist.entrySet();
        Map.Entry<Integer, Double> min = entries.stream().min(Comparator.comparing(Map.Entry::getValue)).get();
        int key = min.getKey();
        minDist.remove(key);
        return key;
    }

    private void relax(EdgeWeightedDiGraph<?> graph, int v) {
        for (DiEdge edge : graph.adj(v)) {
            int w = edge.to();
            if (distTo[v] + edge.weight() < distTo[w]) {
                distTo[w] = distTo[v] + edge.weight();
                edgeTo[w] = edge;
                if (minDist.containsKey(w)) {
                    minDist.replace(w, distTo[w]);
                    System.out.println(w);

                } else {
                    minDist.put(w, distTo[w]);
                }
            }
        }
    }

    public double distTo(int v) {
        return distTo[v];
    }

    public boolean hasPathTo(int v) {
        return distTo[v] != Double.POSITIVE_INFINITY;
    }

    public Iterable<DiEdge> pathTo(int v) {
        if (hasPathTo(v)) {
            LinkedList<DiEdge> path = new LinkedList<>();
            for (DiEdge edge = edgeTo[v]; edge != null; edge = edgeTo[edge.from()]) {
                path.push(edge);
            }
            return path;
        }
        return null;
    }

    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<String>(vertexInfo, edges, weight);
        Dijkstra dijkstra = new Dijkstra(graph, 0);
        for (int i = 0; i < graph.vertexNum(); i++) {
            System.out.print("0 to " + i + ": ");
            System.out.print("(" + dijkstra.distTo(i) + ") ");
            System.out.println(dijkstra.pathTo(i));
        }
    }
}
/* Outputs

0 to 0: (0.0) []
0 to 1: (1.05) [(0->4 0.38), (4->5 0.35), (5->1 0.32)]
0 to 2: (0.26) [(0->2 0.26)]
0 to 3: (0.9900000000000001) [(0->2 0.26), (2->7 0.34), (7->3 0.39)]
0 to 4: (0.38) [(0->4 0.38)]
0 to 5: (0.73) [(0->4 0.38), (4->5 0.35)]
0 to 6: (1.5100000000000002) [(0->2 0.26), (2->7 0.34), (7->3 0.39), (3->6 0.52)]
0 to 7: (0.6000000000000001) [(0->2 0.26), (2->7 0.34)]

*/

和Prim算法的即時(shí)版本的幾乎一樣!兩種算法都是添加邊的方式來構(gòu)造一棵樹:Prim算法每次添加的是離整棵樹(各個(gè)頂點(diǎn))最近的樹外的頂點(diǎn);Dijkstra算法每次添加的是離起點(diǎn)最近的樹外頂點(diǎn)。

Dijkstra不需要marked[]來記錄被訪問過的頂點(diǎn)了,因?yàn)槊織l邊v -> w只會(huì)被放松一次,每個(gè)頂點(diǎn)也只會(huì)放松一次。放松后的頂點(diǎn)的最短路徑長(zhǎng)度一定滿足dist[w] <= distTo[v] + edge.weight(),當(dāng)想重復(fù)放松某個(gè)頂點(diǎn)時(shí),會(huì)因?yàn)闊o法通過以下條件而被跳過。

if (distTo[v] + edge.weight() < distTo[w]) { }

我們還是來跟著圖走一遍。

  • 放松頂點(diǎn)0,2、4被加入Map,distTo[2]為0 -> 2的權(quán)值,distTo[4]為0 -> 4的權(quán)值。
  • 按權(quán)值放松頂點(diǎn)2,0 -> 2添加到樹中。7被加入Map。distTo[7]為0 -> 2 -> 7的權(quán)值和。
  • 放松頂點(diǎn)4,0 -> 4被加入到樹中。5加到Map。dsitTo[5]為0 -> 4 -> 5的權(quán)值和。0 -> 4 -> 7沒有0 ->2 -> 7路徑短所以不更新distTo[7]。
  • 放松頂點(diǎn)7,2- > 7加入到樹中。3加入到Map。distTo[3]為0 -> 2 -> 3 -> 7的權(quán)值和,0 -> 2 -> 7 -> 5的權(quán)值和沒有0 -> 4 -> 5的權(quán)值和小,所有不更新distTo[5]
  • 放松頂點(diǎn)5, 4 ->5加入到樹中,1加入到Map,distTo[1]為0 -> 4 -> 5 -> 1的權(quán)值和。0 -> 4 -> 5 -> 7的權(quán)值和沒有0 -> 2 -> 7的權(quán)值和小,所以不更新distTo[7]
  • 放松頂點(diǎn)3,7 -> 3加入到樹中。6加入到Map。distTo[6]為0 -> 2 -> 7 -> 3 -> 6的權(quán)值和。
  • 放松頂點(diǎn)1,5 -> 1加入到樹。0 -> 4 -> 5 ->1 -> 3的權(quán)值和由于沒有0 -> 2 -> 7 -> 3 的權(quán)值和小,所以不更新distTo[3]。
  • 放松頂點(diǎn)6, 3 -> 6加入到樹中。至此所有頂點(diǎn)都已放松一次,算法結(jié)束。

by @sunhaiyu

2017.9.23

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

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