單源最短路徑-迪傑斯特拉算法(Dijkstra's algorithm)

Dijkstra’s algorithm

迪傑斯特拉算法是目前已知的解決單源最短路徑問題的最快算法.

單源(single source)最短路徑,就是從一個源點出發,考察它到任意頂點所經過的邊的權重之和爲最小的路徑.

這裏寫圖片描述

迪傑斯特拉算法不能處理權值爲負數或爲零的邊,因爲本質上它是一種貪心算法,出現了負數意味着它可能會捨棄一條正確的邊,而選擇一個長邊和一個負數邊,因爲長邊和負數邊的權值之和可能小於那條正確的邊.

算法描述

它的過程也很簡單,按照廣度遍歷的方式考察每一條有向邊(v,w),如果可以對邊進行鬆弛,就記錄這條邊,並更新到邊的目標頂點的最短距離.

注意,這個“最短距離”是當前搜索進程中已知的最短距離而不一定是最終的最短距離.

對邊進行鬆弛的操作如下:

    /**
     * 對邊進行鬆弛
     *
     * 對一條有向邊(v,w,weight)進行考察:
     * 如果當前已知的到w的距離distance_to(w) > distance_to(v) + weight,
     * 說明可以改善到w的距離.從而更新這個距離:
     * distance_to(w) = distance_to(v) + weight;
     * 同時選擇這條邊爲到w的目前已知的最短邊
     * last_edge_to(w) = (v,w,weight)
     * 
     * @param edge 有向帶權邊
     */
    private void relaxEdge(WeightedEdge edge) {
        IndexPriorityQueue<Double> q = indexCrossingEdges;
        int src = edge.src;
        int to =edge.to;
        if(distanceTo[to] > distanceTo[src] + edge.weight) {
            distanceTo[to] = distanceTo[src] + edge.weight;
            lastEdgeTo[to] = edge;
            if(q.contains(to))
                q.decreaseKey(to,distanceTo[to]);
            else
                q.offer(to,distanceTo[to]);
        }
    }

鬆弛操作中,使用了索引式優先隊列來獲取平均O(1)的插入效率和O(logN)的降權(這裏是最小優先隊列,所以decrease-key是提升優先級)效率,請參考:索引式優先隊列

控制算法的搜索方向的是一個循環,這個循環考察隊列是否爲空以判斷是否所有的邊都得到了處理.

同時,在下面的代碼中也可以看出,搜索的方向總是從源點s出發,遍歷它的鄰接表,當耗盡鄰接表時,從優先隊列中出隊和它最近的鄰接點v,接着對v的鄰接表進行搜索.
所以整個搜索方向看上去是在向廣度方向進行的.

        while (!q.isEmpty()) {
            src = q.poll();
            for(Edge e:g.vertices()[src].Adj) {
                relaxEdge((WeightedEdge)e);
            }
        }

可以看出,迪傑斯特拉算法和求最小生成樹的普利姆算法非常相似,因爲它們都是一種基於廣度優先的貪心算法.
關於普利姆算法的分析和實現,請參考:說說最小生成樹

實現代碼

下面給出迪傑斯特拉的完整實現:

/**
 * Created by 浩然 on 4/21/15.
 */
public class Dijkstra {
    /**
     * 當前已知的最短距離,索引爲頂點的編號
     * 比如distanceTo[v]表示當前到頂點v的最短距離
     */
    protected double[] distanceTo;
    /**
     * 當前已知的最短邊,索引爲頂點的編號
     * 比如lastEdgeTo[v]表示當前到頂點v的最短邊
     */
    protected  WeightedEdge[] lastEdgeTo;
    /**
     * 索引式優先隊列,維護到頂點的最短距離
     */
    protected IndexPriorityQueue<Double> indexCrossingEdges;
    /**
     * 有向帶權圖
     */
    private WeightedDirectedGraph g;
    private LinkedList<WeightedEdge> shortestPath;

    /**
     * 單源
     */
    private int src;

    public Dijkstra(WeightedDirectedGraph g){
        this.g = g;
    }

    public void performSP(int src) {
        this.src = src;
        validateEdges();
        resetMemo();
        IndexPriorityQueue q = indexCrossingEdges;

        //從源點開始
        distanceTo[src] = 0;
        q.offer(src,distanceTo[src]);

        while (!q.isEmpty()) {
            src = q.poll();
            for(Edge e:g.vertices()[src].Adj) {
                relaxEdge((WeightedEdge)e);
            }
        }
    }

    /**
     * 對邊進行鬆弛
     *
     * 對一條有向邊(v,w,weight)進行考察:
     * 如果當前已知的到w的距離distance_to(w) > distance_to(v) + weight,
     * 說明可以改善到w的距離.從而更新這個距離:
     * distance_to(w) = distance_to(v) + weight;
     * 同時選擇這條邊爲到w的目前已知的最短邊
     * last_edge_to(w) = (v,w,weight)
     *
     * @param edge 有向帶權邊
     */
    private void relaxEdge(WeightedEdge edge) {
        IndexPriorityQueue<Double> q = indexCrossingEdges;
        int src = edge.src;
        int to =edge.to;
        if(distanceTo[to] > distanceTo[src] + edge.weight) {
            distanceTo[to] = distanceTo[src] + edge.weight;
            lastEdgeTo[to] = edge;
            if(q.contains(to))
                q.decreaseKey(to,distanceTo[to]);
            else
                q.offer(to,distanceTo[to]);
        }
    }

    private void validateEdges() {
        for(Vertex v:g.vertices()) {
            for(Edge e:v.Adj) {
                if(((WeightedEdge) e).weight < 0) {
                    throw new IllegalArgumentException("邊的權值不能爲負!");
                }
            }
        }
    }

    private void resetMemo() {
        int vertexCount = g.vertexCount();
        //重置sp
        shortestPath = new LinkedList<>();
        //重置所有已知最短路徑
        lastEdgeTo = new WeightedEdge[vertexCount];
        //重置所有距離
        distanceTo = new double[vertexCount];
        for(int i = 0; i < distanceTo.length; i++) {
            distanceTo[i] = Double.POSITIVE_INFINITY;
        }
        //重置優先隊列
        indexCrossingEdges = new IndexPriorityQueue<>();
    }

    /**
     * 從源點到目標點是否存在一條路徑
     * @param to 目標點
     * @return 存在則返回真,否則返回假
     */
    private boolean hasPathTo(int to) {
        return distanceTo[to] < Double.POSITIVE_INFINITY;
    }

    public void printShortestPath(int to) {
        if(!hasPathTo(to)){
            System.out.println(String.format("%d-%d 不可達",src,to));
        }

        Stack<WeightedEdge> stack = new Stack<>();
        for(Edge edge = lastEdgeTo[to]; edge != null; ){
            WeightedEdge we = (WeightedEdge)edge;
            stack.push(we);
            edge = lastEdgeTo[we.src];
        }
        System.out.println(String.format("%d-%d的最短路徑:",src,to));

        while (!stack.isEmpty()) {
            WeightedEdge we = stack.pop();
            shortestPath.add(we);
            System.out.println(String.format("%d-%d %.2f",we.src,we.to,we.weight));
        }
    }
}

算法的時間複雜度

對所有的邊要進行考察,所以有O(E ).
每次考察中,要進行隊列的入隊或降權操作,隊列中最多維護V條記錄.最差爲O(logV)
所以最差情況下,時間複雜度爲O(ElogV).

使用斐波那契堆來代替二叉堆實現的優先隊列理論上可以進行有限的優化,因爲這種堆的降權(decrease-key)操作的攤還代價爲O(1 ),但實際上,它過於長的常量時間並不一定能帶來那麼美的效率.

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