並查集(union-find)算法


動態連通性

 

假設程序讀入一個整數對p q,如果所有已知的所有整數對都不能說明p和q是相連的,那么將這一整數對寫到輸出中,如果已知的數據可以說明p和q是相連的,那么程序忽略p q繼續讀入下一整數對.

為了實現這個效果,我們設計並查集這種數據結構來保存程序已知的所有整數對的足夠多的信息,並用它們來判斷一對新對象是否連通,這個問題通俗地叫做動態連通性問題.

 

 

union-find算法的api

 

為了方便,我們把每個對象稱為觸點,使用一個觸點為索引的數組id[]作為基本的數據結構來表示所有分量,對於每個觸點i,用find()方法來判定它分量所需的信息是否保存在id[i]中,connected()方法實現只用了一條語句 find(p) == find(q),它返回一個布爾值.

 

 

算法實現

public class  UF {
    
    private int[] id;    //分量id
    private int count;    //連通分量數目
    
    public UF(int N){
        id = new int[N];
        count = N;
        //初始化分量id數組
        for(int i = 0; i < N; i++){
            id[i] = i;
        }
    }
    
    //連通分量個數
    public int count(){
        return count;
    }
    
    //是否連通
    public boolean connected(int p, int q){
        return find(p) == find(q);
    }
    
    //在p q之間添加一條鏈接
    public void union(int p, int q){
        
    }
    
    //分量標識符
    public int find(int p){
    }
            
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int N = StdIn.readInt();
        UF uf = new UF(N);
        while(!StdIn.isEmpty()){
            int p = StdIn.readInt();
            int q = StdIn.readInt();
            if(uf.connected(p, q)){
                continue;
            }
            uf.union(p, q);
            StdOut.println(p + " " + q);
        }
        StdOut.println(uf.count()+"components");
    }

}
View Code

 

quick-find算法

這種實現方法保證當且僅當 id[p] = id[q] p和q是連通的.即同一連通分量的所有觸點id[]中的值全部相同,這意味着connected()只需判斷id[p]和id[q]的值是否相等即可.

調用union()將p和q歸並到相同的分量中,如果 id[p] == id[q],則不需要進行任何改變,否則遍歷整個數組id,將所有和id[p] 相等的元素變成id[q],當然也可以將所有和id[q]相等的元素變成id[p]——兩者皆可.

 

 

    //在p q之間添加一條鏈接
    public void union(int p, int q){
        //p q已經連通,直接返回
        if(find(p) == find(q))
            return ;
        //遍歷數組,找出和id[p]相等的元素置換成id[q]
        for(int i = 0; i < id.length; i++){
            if(id[i]==find(p)){
                id[i] = find(q);
            }
        }
        count--;
    }
    
    //分量標識符
    public int find(int p){
        return id[p];
    }

 

 

find()操作很快,因為它只需要訪問數組一次,但quick-find算法一般無法處理大型問題,因為對於每一次輸入union()都要掃描整個id[]數組.quick-find算法的運行時間對於最終只能得到少數連通分量的一般應用是平方級的.

 

 

quick-union算法

這個算法提高了union()的速度,它和quick-find算法是互補的.

它也基於相同的數據結構——以觸點作為索引的id[]數組,確切地說,每個id[]元素都是同一個分量中另一個觸點的名字(也可能是自己),由它鏈接到另一個觸點,再由這個觸點鏈接到第三個觸點,如此繼續直到到達一個根觸點,即鏈接指向自己的觸點.

union(p,q)實現很簡單,由p,q分別找到它們的根觸點,然后將一個根觸點鏈接到另一個.和上面一樣,無論是重命名含有p的分量還是重命名含有q的分量都可以.

 

    public int find(int p){
        //找出分量名稱
        while(p!=id[p]){
            p = id[p];
        }
        return p;
    }
    
    public void union(int p, int q){
        //將p和q的根節點統一
        int pRoot,qRoot;
        pRoot = find(p);
        qRoot = find(p);
        if(pRoot==qRoot){
            return ;
        }
        id[pRoot] = qRoot;
        count--;
    }

 

 

加權quick-union算法

與其在union()中隨意將一棵樹連接到另一棵樹,我們現在記錄下每一棵樹的大小並總是將較小的樹連接到較大的樹上.這項改動需要添加一個數組和一些代碼來記錄樹中的節點數,它能夠大大改進算法的效率,我們稱它為加權quick-union算法.

 

public class WeightQuickUnionUF {
    private int[] id;    //父鏈接數組
    private int[] sz;    //(由觸點索引的)各個根節點所對應分量的大小.
    private int count;    //連通分量數目
    
    public WeightQuickUnionUF(int N){
        id = new int[N];
        count = N;
        //初始化id父鏈接數組
        for(int i = 0; i < N; i++){
            id[i] = i;
        }
        //初始化分量大小數組
        for(int i = 0; i < N; i++){
            sz[i] = 1;
        }
    }
    
    //連通分量個數
    public int count(){
        return count;
    }
    
    //是否連通
    public boolean connected(int p, int q){
        return find(p) == find(q);
    }
    
    public int find(int p){
        //找出分量名稱
        while(p!=id[p]){
            p = id[p];
        }
        return p;
    }
    
    public void union(int p, int q){
        //將p和q的根節點統一
        int pRoot,qRoot;
        pRoot = find(p);
        qRoot = find(p);
        if(pRoot==qRoot){
            return ;
        }
        if(sz[pRoot]<sz[qRoot]){
            id[pRoot] = qRoot;
            sz[qRoot] += sz[pRoot];
        }else{
            id[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
        count--;
    }
    
}

 

加權quick-union算法最壞情況是將要被歸並的樹的大小總是相等的(且總是2的冪),這些樹的結構看起來很復雜,但它們都含有2的n次方個節點,因此高度都正好是n.另外,當我們歸並兩個含有2的n次方個節點的樹時,得到的樹含有2的n+1次方個節點,此時樹的高度增加到了n+1,由此推廣我們可以證明quick-union算法能夠保證對數級別的性能.

 

加權qucik-union算法是三種算法中唯一可以用於解決大型問題的算法,它在處理N個觸點和M條連接時最多訪問數組cMlgN次,這個結果和quick-find(以及某些情況下的quick-union算法)需要訪問數組至少MN次形成了鮮明的對比.

 

最優算法 

理想情況下,我們希望每個節點都直接鏈接到它的根節點上,但我們又不希望像union-find算法那樣通過修改大量鏈接做到這一點,這時可以通過檢查節點的同時把它直接鏈接到根節點上面去.

要實現路徑壓縮,只需要為find()添加一個循環,將在路徑上遇到的節點全部鏈接到根節點.

路徑壓縮的加權quick-union算法是最優的算法,但並非所有操作都能在常數時間內完成.

 

    public int find(int p){
        int root = p;
        //找出根節點
        while(root!=id[root]){
            root = id[root];
        }
        while(p!=root){
            int x = p;
            id[x] = root;
            p = id[p];
        }
        return root;
    }

 

 

 

 

各種union-find算法的性能特點

 


免責聲明!

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



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