算法-20-最小生成樹+貪心算法(Prim+Kruskal)

目錄

1、定義

1.1、約定

1.2、貪心算法+切分定理

2、加權無向圖的數據類型

2.1、Edge類

2.2、EdgeWeightedGraph

3、最小生成樹--Prim算法

3.1、Prim算法--方式一

3.2、Prim算法---方法二

4、最小生成樹算法---Kruskal算法


1、定義

加權圖是一種爲每條邊關聯一個權值或是成本的圖模型。

一幅加權圖的最小生成樹(MST)樹中所有邊的權值之和最小 的生成樹。

1.1、約定

在計算圖的最小生成樹的過程中,因爲圖的多種特殊情況,比如負的權值,不連通的情況,會讓我們去做多餘的處理,爲了我們更好的理解最小生成樹的算法, 我們做了下面的約定:

  1. 只考慮連通圖。如果一幅圖是非連通的, 我們只能使用這個算法來計算它的所有連通分量的最小生成樹,合併在一起稱其爲最小生成 森林。
  2. 邊的權重不一定表示距離
  3. 邊的權重可能是 0 或者負數。
  4. 所有邊的權重都各不相同。如果不同邊的權重可 以相同,最小生成樹就不一定唯一了。

1.2、貪心算法+切分定理

貪心算法(又稱貪婪算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的是在某種意義上的局部最優解

而我們圖的最小生成樹算法就是利用了貪心算法的原理,我們只要把圖中連接每個點的最小權值的邊找出來,然後並讓他們連成一棵樹,並且不能出現環或者多棵樹我們就算完成了。

而切分定理就是在貪心算法的基礎上,從起點s出發,把起點s和其他點分成兩部分,然後找出起點s和另外一部分連接的最短路徑(也叫橫切邊)。現在就是兩個點,然後找出另外的部分連接這兩個點的最短路徑(橫切邊)連成三個點,這樣不斷持續下去,就能生成我們的最小生成樹。

切分定理:圖的一種切分是將圖的所有頂點分爲兩個非空且不重疊的兩個集合。橫切邊是一條連接 兩個屬於不同集合的頂點的邊。

2、加權無向圖的數據類型

這裏我們求的加權無向圖的最小生成樹,我們首先要表示出加權無向圖的數據類型, 然後我們才能做下一步的計算。

Edge類來存儲邊、邊的權值、邊的兩個頂點。

EdgeWeightedGraph 類 中有一個 數據鏈表,數組用來存儲每個頂點,鏈表用來存儲每個頂點相連的邊。

2.1、Edge類

public class Edge implements  Comparable<Edge> {

    private final int v;
    private final int w;
    private final double weight;

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

    public double weight() {
        return weight;
    }
    public int either() {
        return v;
    }
    public int other(int vertex){
        if (vertex==v) return w;
        else if (vertex==w) return v;
        else return 0; //這裏應該拋出異常
    }

    @Override
    public int compareTo(Edge that) {
        if (this.weight>that.weight) return 1;
        else if (this.weight<that.weight) return -1;
        else return 0;
    }
}

2.2、EdgeWeightedGraph

public class EdgeWeightedGraph {

    private int V;
    private int E;
    private Bag<Edge>[] adj;

    public EdgeWeightedGraph(int v) {
        V = v;
        adj = new Bag[v];
        for (int i = 0; i < v; i++) {
            adj[i] = new Bag<>();
        }
    }

    private void addEdge(Edge e) {
        int v = e.either();
        int w = e.other(v);
        adj[v].add(e);
        adj[w].add(e);
        E++;
    }
    public int V() {
        return V;
    }
    public int E() {
        return E;
    }
    public Iterable<Edge> adj(int v) {
        return adj[v];
    }
    public Iterable<Edge> edges() {
        Bag<Edge> edges = new Bag<>();
        for (int v = 0; v < V; v++) {
            for (Edge w : adj(v)) {
                if (w.other(v) > v) { //因爲每條邊會被存儲兩次,這個判斷是爲了篩選出一次
                    edges.add(w);
                }
            }
        }
        return edges;
    }
}

3、最小生成樹--Prim算法

Prim算法就是在貪心算法的基礎上,從起點s出發,把起點s和其他點分成兩部分,然後找出起點s和另外一部分連接的最短路徑(也叫橫切邊)。現在就是兩個點,然後找出另外的部分連接這兩個點的最短路徑(橫切邊)連成三個點,這樣不斷持續下去,就能生成我們的最小生成樹。

每次連接都會給樹添加一條邊,知道圖中的所有頂點都被連接到樹中。

3.1、Prim算法--方式一

思路就是把所有邊都添加到一個優先隊列中,然後每次從隊列中拿一條權值最小的邊,判斷該邊的兩個頂點是否都連入了樹中,沒有的話就把該邊連入到樹中。直到所有的頂點連接到樹上。

該Prim 算法計算一幅含有 V 個頂點和 E 條邊的連通加權無向圖的最小生成樹 所需的空間與 E 成正比,所需的時間與 ElogE 成正比(最壞情況)。

public class LazyPrimMST {

    private boolean[] marked; //判斷是否已經走過該點
    private Queue<Edge> mst; //存儲最小生成樹的邊
    private MinPQ<Edge> pq; //優先隊列,每次從隊列中取出最短的邊

    public LazyPrimMST(EdgeWeightedGraph G) {

        marked = new boolean[G.V()];
        mst=new Queue<>();
        pq = new MinPQ<>();
        
        visit(G,0);
        while (!pq.isEmpty()){

            Edge e=pq.delMin(); //取出最短的邊
            int v=e.either();
            int w=e.other(v);
            if (marked[v]&&marked[w]) continue;//如果邊的兩個頂點走過了就進行下一次循環
            mst.enqueue(e);

            if (!marked[v]) visit(G,v); //將沒有放入優先隊列的邊添加進去
            if (!marked[w]) visit(G,w);
        }
    }

    private void visit(EdgeWeightedGraph G, int v) {
        marked[v] = true;
        for (Edge e : G.adj(v)) {
            if (!marked[e.other(v)]) {
                pq.insert(e);
            }
        }
    }
    private Iterable<Edge> mst() {
        return mst;
    }
}

3.2、Prim算法---方法二

方法二的思路 是從起點s出發,將從s到w點的邊 s--->w 標記爲最短路徑的邊,並存入數組中,當我們在遍歷其他點 v時,v點也能通往w,且邊v--->w比 邊s--->w短, 那麼我們就將數組中通往w的邊替換成v--w。

不斷重複,這樣數組中存放就是通往每個點的最短的邊了,因爲這些邊能通往每個頂點,那麼他們必然也能練成一棵樹,通往起點0是沒有邊的。

該Prim 算法的即計算一幅含有 V 個頂點和 E 條邊的連通加權無向圖的最小生成樹所需的空間和 V 成正比,所需的時間和 ElogV 成正比(最壞情況)。

public class PrimMST {

    private boolean[] marked; //判斷是否走過該點
    private Edge[] edgeTo;    //存放最小生成樹的邊
    private double[] distTo;  //存放每天邊的權重
    private IndexMinPQ<Double> pq;  //索引優先隊列,可以通過索引來更改存儲的值

    public PrimMST(EdgeWeightedGraph G) {

        marked = new boolean[G.V()];
        edgeTo = new Edge[G.V()]; //頂點數量和最小生成樹邊的數量一致
        distTo = new double[G.V()]; 
        pq = new IndexMinPQ<>(G.V());

        for (int i = 0; i < G.V(); i++) { //1、先講每條邊的權值設置爲無窮大 
            distTo[i] = Double.POSITIVE_INFINITY;
        }

        distTo[0] = 0.0;           //2、起點0 的權值爲0 ,並將起點0加入到優先隊列中
        pq.insert(0, 0.0);
        while (!pq.isEmpty()) {    //3、每次從隊列中取出最短的邊,並返回邊 v-->w 的頂點w
            visit(G, pq.delMin());
        }
    }

    private void visit(EdgeWeightedGraph G, int v) {

        marked[v] = true;           //4、標記走過了該點
        for (Edge e : G.adj(v)) {   //5、遍歷該點的所有鄰邊

            int w = e.other(v);
            if (marked[w])
                continue;
            if (e.weight() < distTo[w]) {  //6、如果該邊 v-->w 的權值,比前面加入通往w的邊的權值小,則替換 

                edgeTo[w] = e;
                distTo[w] = e.weight();
                if (pq.contains(w))      //7、如果優先隊列已經存了通往w的邊,但不是最短的,則替換
                    pq.change(w, distTo[w]);
                else
                    pq.insert(w, distTo[w]);
            }
        }
    }

    public Iterable<Edge> edges() {
        Queue<Edge> mst = new Queue<>();
        for (int v = 0; v < this.edgeTo.length; ++v) {
            Edge e = this.edgeTo[v];
            if (e != null) {
                mst.enqueue(e);
            }
        }
        return mst;
    }
    public double weight() {
        double weight = 0.0;
        for (Edge e : edges())
            weight += e.weight();
        return weight;
    }
}

4、最小生成樹算法---Kruskal算法

Prim算法的思想是從一個頂點出發不斷地長大成一棵樹。而Kruskal算法的思想是從無數顆小樹不斷合併成一棵大樹。

Kruskal算法的思想是將每條邊都加入優先隊列中,然後每次拿出最小的邊,作爲最小生成樹的一條邊,然後再從中拿出另一條最短的邊,並且這條邊不會和最小生成樹數組中的邊構成環,如果構成環就跳過該條邊,從下一條最短邊開始。這樣就會從無數條短邊開始,不斷合成一棵樹。

Kruskal 算法的計算一幅含有 V 個頂點和 E 條邊的連通加權無向圖的最小生成 樹所需的空間和 E 成正比,所需的時間和 ElogE 成正比(最壞情況)。

public class KruskalMST {
    private static final double FLOATING_POINT_EPSILON = 1.0E-12D;
    private double weight;
    private final Queue<Edge> mst = new Queue<>();

    public KruskalMST(EdgeWeightedGraph G) {

        MinPQ<Edge> pq = new MinPQ<>();

        for (Edge e : G.edges()) {
            pq.insert(e);
        }
        UF uf = new UF(G.V());

        while (!pq.isEmpty() && this.mst.size() < G.V() - 1) {
            Edge e = (Edge) pq.delMin();
            int v = e.either();
            int w = e.other(v);
            if (uf.find(v) != uf.find(w)) {
                uf.union(v, w);
                this.mst.enqueue(e);
                this.weight += e.weight();
            }
        }
    }

    public Iterable<Edge> edges() {
        return this.mst;
    }

    public double weight() {
        return this.weight;
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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