最短路徑:學習總結

最短路徑

最小生成樹是以無向帶權圖爲基準,而最短路徑則是以加權有向圖爲基準

最短路徑樹

  • 給定一幅加權有向圖和一個頂點s。
  • 以s爲起點的一棵最短路徑樹是圖的一幅子圖,它包含s和從s可達的所有頂點。
  • 根節點爲s,到葉子結點的每條路徑和都是有向圖中的一條最短路徑

基本數據結構:

public class DirectedEdge {
    // 定義的是起始節點,互相不一定互通
    private final int v;
    private final int w;
    private final double weight;

    public DirectedEdge(int v, int w, double weight) {
        this.v = v;
        this.w = w;
        this.weight = weight;
    }

    public double weight(){
        return weight;
    }

    public int from(){
        return v;
    }

    public int to(){
        return w;
    }

}
public class EdgeWeightedDigraph {

    // 通過始點來記錄邊
    private final int V;
    private int E;
    private Bag<DirectedEdge>[] adj;

    public EdgeWeightedDigraph(int V) {
        this.V = V;
        this.E = 0;
        adj = (Bag<DirectedEdge>[]) new Bag[V];
        for (int v = 0; v < V; v++) {
            adj[v] = new Bag<>();
        }
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    public void addEdge(DirectedEdge e){
        adj[e.from()].add(e);
        E++;
    }

    public Iterable<DirectedEdge> adj(int v){
        return adj[v];
    }

    public Iterable<DirectedEdge> edges(){
        Bag<DirectedEdge> bag = new Bag<>();
        for (int v = 0; v < V; v++) {
            for (DirectedEdge edge : adj[v]) {
                bag.add(edge);
            }
        }
        return bag;
    }
}

數據結構記錄:

  • 最短路徑樹中的邊:edgeTo[],edgeTo[v]的值爲樹中連接v和它的父節點的邊(也是從s到v的最後一條邊)

  • 到達起點的距離。distTo[],其中distTo[],爲從s到v的已知最短路徑的長度

  • 邊的鬆弛:

    • 檢查從s→w的最短路徑是否先從s→v,然後再由v→w
    • 檢查distTo[v]與e.weight()的和,如果這個值不小於distTo[w],則稱這條邊失效了,並將它忽略
    • 如果這個值更小,就更新內容
private void relax(DirectedEdge e){
    int v = e.from(), w = e.to();
    if(distTo[w] > distTo[v] + e.weight()){
        // 相當於s→v的距離和v到w的邊的權值和更小 -- 比之前的直接從s→w更小
        distTo[w] = distTo[v] + e.weight();
        // 將w的邊替換爲當前的e
        edgeTo[w] = e;
    }
}

通用算法:

  • 放鬆G中的任意邊,知道不存在有效邊爲止。對於任意從s可達的頂點w,在進行這些操作後,distTo[w]的值即爲從s到w的最短路徑長度

Dijkstra算法

算法流程:

  • 首先將distTo[s]初始化爲0,distTo[]中的其他元素初始化爲正無窮,然後將distTo[]最小的非樹頂點放鬆並加入樹中
  • 知道所有的頂點都在樹中或者所有的非樹頂點的distTo[]值均爲無窮大

準備條件

  • distTo[] edgeTo[]數組
  • 索引優先隊列pq;臨時結點 – 保存需要被放鬆的頂點或者確認下一個被放鬆的節點
public class DijkstraSP {

    private DirectedEdge[] edgeTo;
    private double[] distTo;
    private IndexMinPQ<Double> pq;

    public DijkstraSP(EdgeWeightedDigraph G, int s) {

        edgeTo = new DirectedEdge[G.V()];
        distTo = new double[G.V()];
        pq = new IndexMinPQ<>(G.V());

        for (int v = 0; v < G.V(); v++) {
            // 每一個都要賦初值
            distTo[v] = Double.POSITIVE_INFINITY;
            // 每一次都需要添加樹的頂點進隊列,相當於每次找最短路徑都會從頂點開始走一遍
            distTo[s] = 0.0;
            pq.insert(s, 0.0);
            while (!pq.isEmpty()) {
                // 放鬆pq權值最小的點
                relax(G, pq.delMin());
            }
        }
    }

    private void relax(EdgeWeightedDigraph G, int v) {
        for (DirectedEdge e : G.adj(v)) {
            int w = e.to();
            if (distTo[w] > distTo[v] + e.weight()) {
                // 鬆弛邊
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
                // 修改優先隊列中對應的值
                if (pq.contains(w)) pq.changeKey(w, distTo[w]);
                else pq.insert(w, distTo[w]);
            }
        }
    }

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

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

    public Iterable<DirectedEdge> pathTo(int v) {
        validateVertex(v);
        if (!hasPathTo(v)) return null;
        Stack<DirectedEdge> path = new Stack<>();
        for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
            path.push(e);
        }
        return path;
    }

    // 驗證點是否在樹中
    private void validateVertex(int v) {
        int V = distTo.length;
        if (v < 0 || v >= V)
            throw new IllegalArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
    }
}

無環加權有向圖的最短路徑算法

問題變化:要求有權無向圖的最短路徑,相當於創建一個有向圖,每個結點對於相鄰結點都有一個雙向箭頭

  • 核心:
    • 在有向圖的拓撲排序(保證前面的點都指向後面的點)中,對每一條邊進行一次relax鬆弛操作。
    • 鬆弛不改變distTo[v],即s→v的值不再改變,保證v→w的是最小值
public class AcyclicSP {

    private DirectedEdge[] edgeTo;
    private double[] distTo;

    public AcyclicSP(EdgeWeightedDigraph G, int s) {
        edgeTo = new DirectedEdge[G.V()];
        distTo = new double[G.V()];

        for (int v = 0; v < G.V(); v++) 
            // distTo[v] = Double.POSITIVE_INFINITY就等價於marked[v] = false
            distTo[v] = Double.POSITIVE_INFINITY;
       
        distTo[s] = 0.0;
		//構建拓撲排序
        Topological top = new Topological(G);
        // 對於先序序列進行鬆弛
        for (int v : top.order())
            relax(G, v);
    }

    private void relax(EdgeWeightedDigraph G, int v) {
        for (DirectedEdge e : G.adj(v)) {
            int w = e.to();
            if (distTo[w] > distTo[v] + e.weight()) {
                // 鬆弛邊
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
            }
        }
    }
}

拓展:求最長路徑

  • 所有的double最開始記爲最小
  • 鬆弛的判定變爲distTo[w] < distTo[v] + e.weight(),由原來的大於變成小於

一般加權有向圖中的最短路徑問題

Bell–ford算法過程:

  • 在任意含有V個頂點的加權有向圖中給定起點s,s不可達到負權環(各邊權重總和爲負)

  • 將distTo[s]設置爲0,distTo[v]設置爲無窮大,進行v輪邊的放鬆,得到的即是環的最短路徑

  • 邊的總數爲V-1,所以通過V輪迭代後,再進行迭代,總會得到一個相同的結果。結果從第V輪開始收斂

  • 只有上一輪中distTo[v]發生變化的邊,下一輪才能改變其他元素的distTo[]值。通過一個隊列來保存這樣的點

  • 輔助數據結構:

    • 隊列:保存即將被放鬆的頂點的隊列
    • 數組onQ[]:用來指示頂點是否已經存在於隊列中,防止將頂點重複插入隊列
    • 可以保證:
      • 隊列不出現重複的點
      • 在某一輪中,改變了distTo[]和edgeTo[]的值的所有頂點都會在下一輪處理
    • 注意需要在檢測到負權重環的時候退出
public class BellmanFordSP {
    private double[] distTo;
    private DirectedEdge[] edgeTo;
    private boolean[] onQ;
    // 正在被放鬆的頂點
    private Queue<Integer> queue;
    // relax的調用次數
    private int cost;
    // 檢測是否有負權重環
    private Iterable<DirectedEdge> cycle;

    public BellmanFordSP(EdgeWeightedDigraph G, int s) {
        distTo = new double[G.V()];
        edgeTo = new DirectedEdge[G.V()];
        onQ = new boolean[G.V()];
        queue = new LinkedList<>();
        //初始化各邊,以進行放鬆操作
        for (int v = 0; v < G.V(); v++) {
            distTo[v] = Double.POSITIVE_INFINITY;
        }
        distTo[s] = 0.0;
        // 將起點入隊,只要入隊,設置onQ爲true
        queue.offer(s);
        onQ[s] = true;
        // 隊列非空,且不含有負權重環 -- 不然會陷入無限循環
        while (!queue.isEmpty() && !hasNegativeCycle()){
            int v = queue.poll();
            onQ[v] = false;
            // 每次循環,將頂點的出邊進行放鬆
            relax(G, v);
        }

    }

    private void relax(EdgeWeightedDigraph G, int v) {
        for (DirectedEdge e : G.adj(v)) {
            int w = e.to();
            if(distTo[w] > distTo[v] + e.weight()){
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
                // 如果發生了變化,那麼將這個結點入隊
                // 其他distTo不發生變化的結點以後也不會改變
                if(!onQ[w]){
                    queue.offer(w);
                    onQ[w] = true;
                }
            }
            if(cost++ % G.V() == 0){
                // 當遍歷的次數達到V時,進行負權重的檢測
                findNegativeCycle();
            }
        }
    }

    private void findNegativeCycle() {
        int V = edgeTo.length;
        EdgeWeightedDigraph spt = new EdgeWeightedDigraph(V);
        for (int v = 0; v < V; v++) {
            if(edgeTo[v] != null)
                spt.addEdge(edgeTo[v]);
        }
        // 進行環的檢測,只要有環,都是不可接受的,因爲有可能會產生負權重環
        // 所以這裏用的一般的環檢測算法
        EdgeWeightedDirectedCycle cf = new EdgeWeightedDirectedCycle(spt);
        cycle = cf.cycle();
    }

    public boolean hasNegativeCycle() {
        return cycle != null;
    }

    public Iterable<DirectedEdge> negativeCycle(){
        return cycle;
    }
}
// 環檢測算法補充、
public EdgeWeightedDirectedCycle(EdgeWeightedDigraph G) {
    marked  = new boolean[G.V()];
    onStack = new boolean[G.V()];
    edgeTo  = new DirectedEdge[G.V()];
    for (int v = 0; v < G.V(); v++)
        if (!marked[v]) dfs(G, v);

    // check that digraph has a cycle
    assert check();
}

// check that algorithm computes either the topological order or finds a directed cycle
private void dfs(EdgeWeightedDigraph G, int v) {
    onStack[v] = true;
    marked[v] = true;
    for (DirectedEdge e : G.adj(v)) {
        int w = e.to();

        // short circuit if directed cycle found
        if (cycle != null) return;

        // found new vertex, so recur
        else if (!marked[w]) {
            edgeTo[w] = e;
            dfs(G, w);
        }

        // trace back directed cycle
        // 到這裏檢測到了環,開始入棧進行回溯
        else if (onStack[w]) {
            cycle = new Stack<>();

            DirectedEdge f = e;
            while (f.from() != w) {
                // 將點的出邊先入棧
                cycle.push(f);
                // 再獲取點的入邊,直到沒有入邊爲止,即到了環的起點
                f = edgeTo[f.from()];
            }
            cycle.push(f);

            return;
        }
    }

    onStack[v] = false;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章