加權無向圖 - 最小生成樹


1. 最小生成樹的定義

生成樹指的是含有所有頂點的無環連通子圖。注意這其中的三個限定條件:

1)包含了所有的頂點

2)不存在環

3)連通圖

 

如上圖所示。就是一個生成樹。

而最小生成樹指的是所有的邊的權值加起來最小的生成樹。最小生成樹的重要應用領域太多,包括各種網絡問題,例如電力分配網絡,航空、鐵路規划等問題。

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

//帶權重的邊的數據類型
public class Edge implements Comparable<Edge>
{
    int v;
    int w;
    double weight;

    public Edge(int v, int w, double 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 == this.v) return this.w;
        else if(vertex == this.w) return this.v;
        else throw new RuntimeException("Inconsistent edge");

    }

    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);
    }

}

以上為帶權重的邊的構造函數。

其中兩個函數,either() 和 other() 目前比較難以理解,但之后會有比較大的用處。

 

//加權無向圖
public class EdgeWeightedGraph
{
    private int V;
    private int E;
    private Bag<Edge>[] adj; //注意這里的鄰接矩陣不再是鄰接的點了,而是鄰接的邊的集合

    public EdgeWeightedGraph(int V)
    {
        this.V = V;
        E = 0;
        adj = (Bag<Edge>[]) new Bag[V];
        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> b = new Bag<Edge>();
        for(int v = 0;v < V;v++)
        {
            for(Edge e: adj[v])
                if(e.other(v) > v) b.add(e);
        }
        return b;
    }
}

 

3. 尋找最小生成樹

首先要了解,假設所有的邊的權重不相同的情況下,每幅連通圖都只有唯一的一顆最小生成樹。而且最小生成樹有一個很重要的切分定理。

切分定理:在一副加權圖中,給定任意的切分,它的橫切邊中權重最小者必然屬於最小生成樹。也就是說假如把所有頂點任意的分成兩堆,那么能夠把兩堆頂點中連起來的權重最小的邊肯定屬於最小生成樹。

從上圖中可以看出,在兩堆頂點中,能夠把7和0連起來的邊的權重(在這里可以看成是距離)是最小的,所以這條邊肯定屬於最小生成樹。

要把整顆生成樹找出來,可以用貪心算法,一條邊一條邊找出來加到生成樹中。

4. prim算法

首先需要厘清prim算法中用到的數據結構。

1)樹中頂點的集合marked[],假如marked[v] = true,則說明,v此時已經在樹中。

2)樹中的邊。一條隊列mst來存儲當前樹中的所有邊。

3)橫切邊。一條優先隊列MinPQ<Edge>來存儲當前考慮的可能在最小生成樹中的候選邊。

 

prim也是貪心算法的一種。具體過程如下:

1)先從任意一個頂點開始,把該頂點的所有鄰接邊加入優先隊列MinPQ中,並把該頂點加入最小生成樹中的marked數組。

2)在優先隊列MinPQ中權重最小的那條邊,把這條邊加入最小生成樹mst中,並把邊的另一個頂點加入marked數組,並把新加入的頂點的鄰接邊也加入到優先隊列MinPQ中。

3)繼續在優先隊列MinPQ中找到權重最小的邊,並把該條邊加入mst中,並繼續把邊的另一個頂點計入marker數組,並把新加入的頂點的鄰接邊也加入到優先隊列MinPQ中。

4)...不斷重復當前過程。

所以prim算法的效果就相當於從某一點出發,不斷在當前的樹的基礎上延伸新的邊,最后形成一顆完整的樹。

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<Edge>();
        pq = new MinPQ<Edge>();

        visit(G, 0);
        while(!pq.isEmpty()) //注意循環條件,等到最小生成樹完全生成后,最后MinPQ中會一條一條被刪掉,直到為空
        {
            Edge e = pq.delMin();

            int v = e.either(), 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);
    }
 }

但是需要注意的是,以上的代碼實現過程中,優先隊列會先保留失效的邊,到最后再一條一條刪掉,這種方法又被稱為延時實現。

還有另一種即時實現的方法。這種方法的核心在於,我們只在優先隊列中保存每個非樹頂點的一條邊,也就是離樹頂點最近的那條邊。其余的邊都會失效,沒有必要保留它們。

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 v= 0;v<G.V(); v++)
            distTo[v] = Double.POSITIVE_INFINITY;

        pq.insert(0, 0.0);
        distTo[0] = 0.0;
        while(!pq.isEmpty())
            visit(G, pq.delMin());
    }

    public void visit(EdgeWeightedGraph G, int v)
    {
        marked[v] = true;
        for(Edge e: G.adj[v])
        {
            int w = e.other(v);
            if(marked[w]) continue;
            if(e.weight() < distTo[w])
            {
                distTo[w] = e.weight();
                edgeTo[w] = e;
                if(pq.contains(w)) pq.change(w, distTo[w]);
                else pq.insert(w, distTo[w]);
            }
            
        }
    }
}

 

5. kruskal算法

於prim算法是在一顆小樹的基礎上,一條邊一條邊延展從而生成一顆大樹不同,kruskal算法則是全面開花,最后連成一棵樹。

kruskal的算法思想也很簡單:按照權重將邊排序依次添加進樹中,如果邊會形成環,則棄之,否則,加入樹中。

 

public class kruskalMST
{
    private Queue<Edge> mst;

    public kruskalMST(EdgeWeightedGraph G)
    {
        
        mst = new Queue<Edge>();
        MinPQ<Edge> pq = new MinPQ<Edge>();
        UF uf = new UF();
        
        for(Edge e:G.edges()) pq.insert(e);
        while(!pq.isEmpty() && mst.size() < G.V() - 1)
        {
            Edge e = pq.delMin();
            int v = e.either(), w = e.other(v);
            if(uf.connected(v, w)) continue; //如果形成了環,就跳過
            uf.union(v, w);                  //合並分量
            mst.enqueue(e);                  //添加到樹中
        }
    }
    
    public Iterable<Edge> edges()
    { return mst;}
}

 

 

參考資料:《算法》第四版

 


免責聲明!

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



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