數據結構與算法--最短路徑之Dijkstra算法

數據結構與算法--最短路徑之Dijkstra算法

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

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

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

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

加權有向圖的實現

首先是有向邊。

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類,更簡單些,因為兩個頂點有明顯的先后順序。

然后是加權有向圖。

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;
    // 頂點信息
    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++;
    }

    // 返回與某個頂點依附的所有邊
    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("個頂點, ").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());

    }
}

實現和加權無向圖差不多,就改了addEdgeadj方法。addEdge由于邊有向,不會對稱地存儲邊;adj方法不像無向圖那樣鄰接表中有重復的邊,有向圖中鄰接表中的邊都是唯一的,所以全部加入即可。

最短路徑的數據結構

1、最短路徑樹中的邊

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

2、起點到各個頂點的最短距離

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

最短路徑算法的基礎基于一個被稱為松弛的簡單操作。放松一條邊v -> w意味著檢查s到w的最短路徑是否是 先從s到v,再從v到w。如果是就更新相關數據結構的內容;如果不是,不作更改。用代碼可以表示為

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

再用一幅圖加深理解。

先看左邊兩個圖:s到v的最短距離是3.1,s到w的最短距離是3.3。當在頂點v時,檢查它的鄰接點w,邊v -> w的權值是1.3,從s到w的當然不能先從s到v,再從v到w,因為這倆加起來都4.4,比原來s到w的方案還要費勁,所以不會更改distTo[w]edgeTo[w]。此時我們說v -> w這條邊失效并忽略它。

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

對頂點的放松就是:放松由該頂點引出的所有邊

在實現之前,對于最短路徑算法我們需要了解得更多,來看幾個命題。

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

Dijkstra算法的實現

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
        }
        // 到起點距離為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算法的即時版本的幾乎一樣!兩種算法都是添加邊的方式來構造一棵樹:Prim算法每次添加的是離整棵樹(各個頂點)最近的樹外的頂點;Dijkstra算法每次添加的是離起點最近的樹外頂點。

Dijkstra不需要marked[]來記錄被訪問過的頂點了,因為每條邊v -> w只會被放松一次,每個頂點也只會放松一次。放松后的頂點的最短路徑長度一定滿足dist[w] <= distTo[v] + edge.weight(),當想重復放松某個頂點時,會因為無法通過以下條件而被跳過。

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

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

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

by @sunhaiyu

2017.9.23

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

推薦閱讀更多精彩內容