算法_最小生成樹


一.概述

      加權無向圖是一種在無向圖的基礎上,為每條邊關聯一個權值或是成本的圖模型.應用可以有很多:例如在一幅航空圖中,邊表示導線,權值則表示導線的長度或是成本等.

  圖的生成樹是它的一顆含有其所有頂點的無環連通子圖,一幅加權圖的最小生成樹(MST)是它的一顆權值(樹中的所有邊的權值之和)最小的生成樹.下圖為一幅加權無向圖和它的最小生成樹.(箭頭不指示方向,標紅的為最小生成樹).

                                                                                 

二.原理

  1.圖的一種切分是將圖的所有頂點分為兩個非空且不重疊的兩個集合.橫切邊是連接兩個屬於不同集合的頂點的邊.

    2.切分定理:在一幅加權圖中,給定任意的切分,它的橫切邊中的權重最小者必然屬於最小生成樹.(假設命題不成立,假設權重最小的邊為e,將它加入最小生成樹必然就會成環,那么這個環應該還有另一條橫切邊,而該橫切邊f必然權重>e,那么就可以刪除f,得到e,來獲取權重更小的生成樹.就和最小生成樹矛盾,因此命題成立).

三.貪心算法

      初始狀態下,一個含有V個頂點的無向圖所有的邊都不為黑色,找到一種切分,它所產生的橫切邊均不為黑色,橫切邊中權值最小的邊標記為黑色(放入最小生成樹),如此往復,直到標記了V-1條邊.(根據切分定理很容易證明),圖的貪心算法的執行流程如下,箭頭不指示方向,黑色加粗的邊為最小生成樹的邊.

                          

                          

                            

                            

                            

                              

                              

                             

四.加權無向圖的數據類型表示.

 1.帶權重的邊的數據類型:

  邊的數據類型記錄了邊的兩點,並且給出了獲取邊其中的某一點,以及根據某點獲取另外一個點的方法.同時也用了一個weight的變量來記錄邊的權重.邊的數據結構如下:

//帶權重的邊的數據類型
public class Edge implements Comparable<Edge>{
    private final int v;    //頂點之一
    private final int w;    //另一個頂點
    private final double weight; //權重
    public Edge(int v, int w, double weight) {
        super();
        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 throw new RuntimeException("Inconsisitent edge");
    }
    @Override
    public int compareTo(Edge that) {
        if(this.weight<that.weight) return -1;
        else if(this.weight>that.weight) return 1;
        else return 0;
    }
    public String toString() {
        return String.format("%d%-d %.2f", v,w,weight);
    }
    
}

2.加權無向圖的數據類型:

  這個數據類型和Graph的API基本相似,不同的是,這個API的基礎是Edge且添加了一個edges方法.加權無向圖的代碼如下:

public class EdgeWeightedGraph {
    private final int V;    //頂點總數
    private int E;            //邊的總數
    private Bag<Edge> [] adj;    //鄰接表
    
    public EdgeWeightedGraph(int V) {
        this.V=V;
        this.E=0;
        adj=(Bag<Edge> [])new Bag[V];//和Queue不同,Bag保證無序
        for(int v=0;v<V;v++) {
            adj[v]=new Bag<Edge>();
        }
    }
    public int V() {return V; }
    public int E() {return E; }
    public void addEdge(Edge e) {
        int v=e.either();
        int w=e.other(v);
        adj[v].add(e);
        adj[w].add(e);
        E++;
    }
    public Iterable<Edge> adj(int v) {
        return adj[v];
    }
     public Iterable<Edge> edges() {
            Bag<Edge> list = new Bag<Edge>();
            for (int v = 0; v < V; v++) {
                int selfLoops = 0;
                for (Edge e : adj(v)) {
                    if (e.other(v) > v) {
                        list.add(e);
                    }
                    else if (e.other(v) == v) {
                        if (selfLoops % 2 == 0) list.add(e);
                        selfLoops++;
                    }
                }
            }
            return list;
        }
}

五.Prim算法

   Prim算法為計算最小生成樹提供了實現.它的每一步都為樹添加一條邊.每一次都將下一條連接樹中的頂點和不在樹中的頂點且權重最小的邊加入樹中.代碼如下所示:

//最小生成樹的延遲Prim算法
/**
 * 先對樹上的第0個點進行標記.
 * 然后遍歷第0個點的所有相鄰頂點.
 * 如果沒有被標記的話.那么就把對應的邊添加進去MinPQ
 * 在MinPQ中移除權重最小的(如果兩個頂點都被標記的話,那么直接繼續)
 * 沒有被標記的話,直接visit,標記,然后添加到MinPQ中
 * @author Administrator
 *
 */
public class LazyPrimMST {
    private boolean[] marked;    //最小生成樹的頂點
    private Queue<Edge> mst;    //最小生成樹的邊.
    private MinPQ<Edge> pq;     //橫切邊(其中包括失效的邊)
    public LazyPrimMST(EdgeWeightedGraph G) {
        pq=new MinPQ<Edge>();
        marked=new boolean[G.V()];
        mst=new Queue<Edge>();
        visit(G,0);        //假設G是連通的
        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) {
        //標記頂點v並將所有連接v和未被標記的頂點的邊加入pq
        marked[v]=true;
        for(Edge e:G.adj(v)) {
            if(!marked[e.other(v)]) pq.insert(e);
        }
    }
    public Iterable<Edge> edges() {
        return mst;
    }
}

  之所以稱為延遲算法,這是因為MinPQ中保存了大量的無效邊.

六.Prim算法的即時實現

   Prim算法的即時實現遵循下面的思路:只會在優先隊列中保存每個非樹頂點w的一條邊.采用索引優先隊列,將頂點和邊關聯起來.該邊為將它與樹中的頂點連接起來的權重最小的邊.

  PrimMST類有兩個數組edgeTo,distTo,他們具有以下性質:

  1.如果頂點v不在樹中但至少含有一條邊與樹相連接,那么edgeTo[v]是與樹相連接的權重最小的邊,distTo[v]是該邊的權重.

  2.這類點都存放在索引優先隊列中.

  該算法的基本步驟是PrimMST會從優先隊列中取出一個頂點v,並且檢查與它相連的點v-w,如果w被標記過,則失效,如果w不在優先隊列中或者v-w權重小於目前已知的最小值edgeTo[w],代碼會更新數組,將v-w作為樹的最佳選擇.代碼如下:

 

/**
 * 先將0加入最小索引隊列中.然后通過對於G.adj(v)進行遍歷.
 * 獲得不同的邊,觀察頂點連接到樹的邊是否小於distTo保存的數值.
 * 如果小於的話,那么添加進去.或者修改.
 * 不小於不變.一個個點進行遍歷.不斷獲得各個點到樹的最小的距離.
 * 然后將距離最小的點加進去
 * 
 * @author Administrator
 *
 */
public class PrimMST {
    private Edge[] edgeTo;                //距離樹最近的邊
    private double[] distTo;            //distTo[w]=edgeTo[w].weight()
    private boolean[] marked;            //如果v在樹中則為true
    private IndexMinPQ<Double> pq;        //有效的橫切邊
    
    public PrimMST(EdgeWeightedGraph G) {
        edgeTo=new Edge[G.V()];
        distTo=new double[G.V()];
        marked=new boolean[G.V()];
        for(int v=0;v<G.V();v++) {
            distTo[v]=Double.POSITIVE_INFINITY;
        }
        pq=new IndexMinPQ<Double>(G.V());
        distTo[0]=0.0;
        pq.insert(0, 0.0);
        while(!pq.isEmpty()) {
            visit(G,pq.delMin());//每次都取出權重最小的邊
        }
    }

    private void visit(EdgeWeightedGraph G, int v) {
        marked[v]=true;
        for(Edge e:G.adj(v)) {
            int w=e.other(v);
            if(marked[w]) continue;    //v-w失效
            if(e.weight()<distTo[w]) {
                edgeTo[w]=e;
                distTo[w]=e.weight();
                if(pq.contains(w)) pq.changeKey(w, distTo[w]);
                else pq.insert(w, distTo[w]);
            }
        }
    }
     public Iterable<Edge> edges() {
            Queue<Edge> mst = new Queue<Edge>();
            for (int v = 0; v < edgeTo.length; v++) {
                Edge e = edgeTo[v];
                if (e != null) {
                    mst.enqueue(e);
                }
            }
            return mst;
        }

}

七.Kruskal算法

  Kruskal算法的思想按照邊的權重順序從小到大處理它們,然后將邊按照從小到大的次序加入到最小生成樹中,加入的邊不會和已經加入的邊構成環,直到樹中含有v-1條邊為止.原理在於如果加入的邊不會和已有的邊構成環,那么它就是一個跨越了所有和樹頂點相鄰的頂點組成的集合以及他們的補集所構成的切分.而這個切分的權重又是最小的,根據切分定理,它一定是最小生成樹的一條邊.

  代碼的實現如下所示,代碼中用了UF判斷是否構成環,並且將一個兩個點相連,使之共同構成在同一個連通分量.

/**
 * 首先將所有的邊都加入到MinPQ中.
 * 然后根據權值從小到大依次加入.
 * 如果該邊已經在最小生成樹里,就忽略.
 * 如果不在,那么就加入進去(union方法).直到mst的size達到v-1
 * 原則:如果下一條被加入的邊不會和已有的形成環.那么它
 * 就相當於是一個切分,而它的權重又是最小的.因此可以加入進去
 * @author Administrator
 *
 */
public class KruskalMST {
    private Queue<Edge> mst;
    public KruskalMST(EdgeWeightedGraph G) {
        mst=new Queue<Edge>();
        MinPQ<Edge> pq=new MinPQ<>();
        for(Edge e:G.edges()) {
            pq.insert(e);
        }
        UF uf=new UF(G.V());
        while(!pq.isEmpty()&&mst.size()<G.V()-1) {
            Edge e=pq.delMin();
            int v=e.either();
            int w=e.other(v);
            if(uf.connected(v, w)) continue;    //處於同一個連通分量中.
            uf.union(v, w);
            mst.enqueue(e);
        }
    }
    public Iterable<Edge> edges() {
        return mst;
    }
    
}
public class UF {
    private int[] id;        //分量id
    private int count;        //分量數量
    public UF(int N) {
        count=N;
        id=new int[N];
        for(int i=0;i<N;i++) {
            id[i]=i;
        }
    }
    //連通分量的數量
    public int count() {
        return count;
    }
    //如果pq存在於同一個連通分量中,則返回true
    public boolean connected(int p,int q) {
        return find(p)==find(q);
    }
    //p所在的連通分量標識符
    public int find(int p) {
        // TODO Auto-generated method stub
        return id[p];
    }
    //在p,q之間建立連接(QuickFind算法)
    public void union(int p,int q) {
        //將p和q歸並到相同的分量中
        int pID=find(p);
        int qID=find(q);
        //如果已經在相同的分量中則不采取行動
        if(pID==qID) return ;
        for(int i=0;i<id.length;i++) {
            if(id[i]==pID) id[i]=qID;
        }
        count--;
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM