1. 程式人生 > 其它 >加權有向圖和最短路徑

加權有向圖和最短路徑

加權有向圖

之前學習的加權無向圖中,邊是沒有方向的,並且同一條邊會同時出現在該邊的兩個頂點的鄰接表中,為了能夠處 理含有方向性的圖的問題,我們需要實現以下加權有向圖。

加權有向圖邊的表示

類名 DirectedEdge
構造方法 DirectedEdge(int v,int w,double weight):通過頂點v和w,以及權重weight值構造一個邊物件
成員方法 1.public double weight():獲取邊的權重值
2.public int from():獲取有向邊的起點
3.public int to():獲取有向邊的終點
成員變數 1.private final int v:起點
2.private final int w:終點
3.private final double weight:當前邊的權重

程式碼:

/**
 * 有向邊
 * @author wen.jie
 * @date 2021/8/30 14:39
 */
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 this.weight;
    }

    //起點
    public int from() {
        return v;
    }

    //終點
    public int to() {
        return w;
    }
    
    @Override
    public String toString() {
        return "DirectedEdge{" +
                "v=" + v +
                ", w=" + w +
                ", weight=" + weight +
                '}';
    }
}

加權有向圖的實現

類名 EdgeWeightedDigraph
構造方法 EdgeWeightedDigraph(int V):建立一個含有V個頂點的空加權有向圖
成員方法 1.public int V():獲取圖中頂點的數量
2.public int E():獲取圖中邊的數量
3.public void addEdge(DirectedEdge e):向加權有向圖中新增一條邊e
4.public Queue adj(int v):獲取由頂點v指出的所有的邊
5.public Queue edges():獲取加權有向圖的所有邊
成員變數 1.private final int V: 記錄頂點數量
.private int E: 記錄邊數量
3.private Queue[] adj: 鄰接表

程式碼:

/**
 * 加權有向圖
 * @author wen.jie
 * @date 2021/8/30 14:42
 */
public class EdgeWeightedDigraph{

    private final int V;

    private int E;

    private Queue<DirectedEdge>[] adj;

    public EdgeWeightedDigraph(int V) {
        this.V = V;
        this.E = E;

        this.adj = new Queue[V];
        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<>();
        }
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    //向加權有向圖中新增一條邊
    public void addEdge(DirectedEdge e) {
        int v = e.from();
        adj[v].enqueue(e);
        E++;
    }

    //獲取由頂點v指出的所有的邊
    public Queue<DirectedEdge> adj(int v) {
        return adj[v];
    }

    //獲取加權有向圖中的所有邊
    public Queue<DirectedEdge> edges() {
        Queue<DirectedEdge> allEdges = new Queue<>();
        for (int v = 0; v < V; v++) {
            for (DirectedEdge edge : adj[v]) {
                allEdges.enqueue(edge);
            }
        }
        return null;
    }
}

最短路徑

最短路徑定義及性質

定義: 在一副加權有向圖中,從頂點s到頂點t的最短路徑是所有從頂點s到頂點t的路徑中總權重最小的那條路徑。

性質:

1.路徑具有方向性;

2.權重不一定等價於距離。權重可以是距離、時間、花費等內容,權重最小指的是成本最低

3.只考慮連通圖。一副圖中並不是所有的頂點都是可達的,如果s和t不可達,那麼它們之間也就不存在最短路徑,為了簡化問題,這裡只考慮連通圖。

4.最短路徑不一定是唯一的。從一個頂點到達另外一個頂點的權重最小的路徑可能會有很多條,這裡只需要找出一條即可。

最短路徑樹:給定一副加權有向圖和一個頂點s,以s為起點的一棵最短路徑樹是圖的一副子圖,它包含頂點s以及從s可達的所有頂點。這棵有向樹的根結點為s,樹的每條路徑都是有向圖中的一條最短路徑。

最短路徑樹API設計

計算最短路徑樹的經典演算法是dijstra演算法,為了實現它,先設計如下API:

類名 DijkstraSP
構造方法 public DijkstraSP(EdgeWeightedDigraph G, int s):根據一副加權有向圖G和頂點s,建立一個計算頂點為s的最短路徑樹物件
成員方法 1.private void relax(EdgeWeightedDigraph G, int v):鬆弛圖G中的頂點v
2.public double distTo(int v):獲取從頂點s到頂點v的最短路徑的總權重
3.public boolean hasPathTo(int v):判斷從頂點s到頂點v是否可達
4.public Queue pathTo(int v):查詢從起點s到頂點v的最短路徑中所有的邊
成員變數 1.private DirectedEdge[] edgeTo: 索引代表頂點,值表示從頂點s到當前頂點的最短路徑上的最後一條邊
2.private double[] distTo: 索引代表頂點,值從頂點s到當前頂點的最短路徑的總權重
3.private IndexMinPriorityQueue<Double> pq:存放樹中頂點與非樹中頂點之間的有效橫切邊

鬆弛技術

鬆弛這個詞來源於生活:一條橡皮筋沿著兩個頂點的某條路徑緊緊展開,如果這兩個頂點之間的路徑不止一條,還有存在更短的路徑,那麼把皮筋轉移到更短的路徑上,皮筋就可以放鬆了。

鬆弛這種簡單的原理剛好可以用來計算最短路徑樹。

在我們的API中,需要用到兩個成員變數edgeTo和distTo,分別儲存邊和權重。一開始給定一幅圖G和頂點s,我們只知道圖的邊以及這些邊的權重,其他的一無所知,此時初始化頂點s到頂點s的最短路徑的總權重disto[s]=0;頂點s到其他頂點的總權重預設為無窮大,隨著演算法的執行,不斷的使用鬆弛技術處理圖的邊和頂點,並按一定的條件更新edgeTo和distTo中的資料,最終就可以得到最短路徑樹。

邊的鬆弛:

放鬆邊v->w意味著檢查從s到w的最短路徑是否先從s到v,然後再從v到w?

如果是,則v-w這條邊需要加入到最短路徑樹中,更新edgeTo和distTo中的內容:edgeTo[w]=表示v->w這條邊的DirectedEdge物件,distTo[w]=distTo[v]+v->w這條邊的權重;

如果不是,則忽略v->w這條邊。

頂點的鬆弛:

頂點的鬆弛是基於邊的鬆弛完成的,只需要把某個頂點指出的所有邊鬆弛,那麼該頂點就鬆弛完畢。例如要鬆弛頂點v,只需要遍歷v的鄰接表,把每一條邊都鬆弛,那麼頂點v就鬆弛了。

如果把起點設定為頂點0,那麼找出起點0到頂點6的最短路徑0->2->7>3->6的過程如下:

Dijstra演算法實現

Disjstra演算法的實現和Prim演算法很類似,構造最短路徑樹的每一步都是向這棵樹中新增一條新的邊,而這條新的邊是有效橫切邊pq佇列中的權重最小的邊。

程式碼:

/**
 * Dijkstra演算法
 * @author wen.jie
 * @date 2021/8/30 15:14
 */
public class DijkstraSP {

    //索引代表頂點,值表示從頂點s到當前頂點的最短路徑上的最後一條邊
    private DirectedEdge[] edgeTo;

    //索引代表頂點,值從頂點s到當前頂點的最短路徑的總權重
    private double[] distTo;

    //存放樹中頂點與非樹中頂點之間的有效橫切邊
    private IndexMinPriorityQueue<Double> pq;

    public DijkstraSP(EdgeWeightedDigraph G, int s){
        this.edgeTo = new DirectedEdge[G.V()];
        this.distTo = new double[G.V()];
        Arrays.fill(distTo, Double.POSITIVE_INFINITY);
        pq = new IndexMinPriorityQueue<>(G.V());

        distTo[s] = 0.0;
        pq.insert(s, 0.0);
        while (!pq.isEmpty()) {
            relax(G, pq.delMin());
        }
    }

    //鬆弛圖G中的頂點v
    private void relax(EdgeWeightedDigraph G, int v){
        for (DirectedEdge edge : G.adj(v)) {
            int w = edge.to();
            //鬆弛技術
            if (distTo(v) + edge.weight() < distTo(w)){
                distTo[w] = distTo[v] + edge.weight();
                edgeTo[w] = edge;
                if (pq.contains(w)) {
                    pq.changeItem(w, distTo(w));
                } else {
                    pq.insert(w, distTo(w));
                }
            }
        }
    }

    //獲取從頂點s到頂點v的最短路徑的總權重
    public double distTo(int v){
        return distTo[v];
    }

    //判斷從頂點s到頂點v是否可達
    public boolean hasPathTo(int v){
        return distTo[v] < Double.POSITIVE_INFINITY;
    }

    //查詢從起點s到頂點v的最短路徑中所有的邊
    public Queue<DirectedEdge> pathTo(int v){
        if (!hasPathTo(v)){
            return null;
        }
        Queue<DirectedEdge> allEdges = new Queue<>();
        while (true) {
            DirectedEdge e = edgeTo[v];
            if (e == null)
                break;
            allEdges.enqueue(e);
            v = e.from();
        }
        return allEdges;
    }

}

測試:

    String[] strs = new String[]{
            "4 5 0.35",
            "5 4 0.35",
            "4 7 0.37",
            "5 7 0.28",
            "7 5 0.28",
            "5 1 0.32",
            "0 4 0.38",
            "0 2 0.26",
            "7 3 0.39",
            "1 3 0.29",
            "2 7 0.34",
            "6 2 0.40",
            "3 6 0.52",
            "6 0 0.58",
            "6 4 0.93"
    };

    @Test
    public void test() {
        EdgeWeightedDigraph G = new EdgeWeightedDigraph(8);
        for (String str : strs) {
            String[] split = str.split(" ");
            DirectedEdge e = new DirectedEdge(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Double.parseDouble(split[2]));
            G.addEdge(e);
        }

        DijkstraSP dijkstraSP = new DijkstraSP(G, 0);
        Queue<DirectedEdge> edges = dijkstraSP.pathTo(6);
        for (DirectedEdge edge : edges) {
            System.out.println(edge);
        }
    }

本文所有程式碼均已上傳:https://gitee.com/wj204811/algorithm