數據結構及算法基礎--並查集(union-find)


並查集,在一些有N個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然后按一定順序將屬於同一組的元素所在的集合合並,其間要反復查找一個元素在哪個集合中。這一類問題近幾年來反復出現在信息學的國際國內賽題中,其特點是看似並不復雜,但數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受;即使在空間上勉強通過,運行的時間復雜度也極高,根本就不可能在比賽規定的運行時間(1~3秒)內計算出試題需要的結果,只能用並查集來描述。

首先我們給出並查集(union-find)的api:

在《algorithm》書中,對與uf的應用直接給出了代碼,但是其中find和union方法並沒有直接實現,而是空着的。書中代碼如下:

 

注意:這里看見有一個StdIn的類作為類似於輸入流的作用,這個在java中是不存在的,這是這本書自己構造的一個類。相信以后在這本書中也會有類似的部分,可以在網上找到其中的具體實施代碼,我會發送出來的。

 

為什么不直接實現find和union方法呢,這里會考慮到數據結構和算法復雜度的問題。

我們將其分為:quickunion,quickfind,weightedquickunion,weightedquickunion with path compression;

1)Quick-Find

其中判斷p和q兩個元素是否連接便是讓兩個元素的id[]相同。

此時,find()便是直接返回id[p]的值

而union()便是將連接的兩個集合的id[]變為一致:當然,我們首先要判斷她們是不是已經連接;

 

由上圖便可看出,我們union(3,4),得到兩者的id[]相同,即id[3]=id[4]=3(id值為連接的集合包含的任意一個數);

得到的代碼如下:

package unionFind;

public class QuickFindUF {
    private int id[];
    private int count;
    
    public QuickFindUF(int N){
        count=N;
        id=new int[N];
        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);
    }
    
    public int find(int p){
        return id[p];
    }
    public void union(int p,int 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--;
    }
}

 

 

2)Quick-Union

Quick-Union的結構如下圖所示:

即每一個結點的id為上一個結點。而根節點便是root=id[root];

判斷兩個元素是否連接則是判斷兩個元素的根(root)是否相同。

find()則為找到元素的根節點;

union(p,q)即將p的根節點與q的根節點連接。

代碼實現如下:

package unionFind;

public class QuickUnionUF {
    private int id[];
    private int count;
    
    public QuickUnionUF(int N){
        id=new int[N];
        count=N;
        for(int i=0;i<N;i++){
            id[i]=i;
        }
    }
    
    public int count(){
        return count;
    }
    
    public int find(int i){
        while(i!=id[i]){i=id[i];}
        return i;
    }
    
    public void union(int p,int q){
        if(find(p)==find(q))return;
        id[find(p)]=find(q);
        count--;
    }
    
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }
}

 

3)Weighted Quick-Union

當然,我們如果胡亂的將根節點相互連接,會導致這個樹的結構非常糟糕,比如:

我們可以看到這個樹的結構非常非常糟糕。

為了避免這個情況,我們記錄樹的大小,並且總是將小的樹連接到大的樹:

使用這種方法可以很大程度的優化樹的結構,例如上圖的樹我們可以變為:

 

具體實現代碼如下:

package unionFind;

public class WeightedQuickUnionUF {
    private int id[];
    private int count;
    private int sz[];
    
    public WeightedQuickUnionUF(int N){
        count=N;
        id=new int[N];
        sz=new int[N];
        for(int i=0;i<N;i++){
            id[i]=i;
            sz[i]=1;
        }
    }
    
    public int find(int p){
        while(p!=id[p])p=id[p];
        return p;
    }
    
    public void union(int p,int q){
        int pid=find(p);
        int qid=find(q);
        if(qid==pid)return ;
        if(sz[pid]<sz[qid]){
            id[pid]=qid;
            sz[qid]+=sz[pid];
        }
        else{
            id[qid]=pid;
            sz[pid]+=sz[qid];
        }
        count--;
    }
    
    public int count(){
        return count;
    }
    
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }
}

 

4)Weighted Quick-Union with Path Compression

最優情況下,我們希望所有的節點都直接連接到根節點上,但是又不希望像QuickUnion那樣大量修改連接,這時,我們可以在檢查節點的同時將它與根節點直接連接。

 

例如,我們對下列並查集進行union(7,3);

 

在采取最優算法下,結果如下:

 

可以看出,我們將遍歷到的節點都直接與根節點直接連接,這一切只需要在find內的循環進行修改就可以實現。

具體的代碼如下:

package unionFind;

public class WeightedQuickUnionUFWPC {
    private int id[];
    private int count;
    private int sz[];
    
    public WeightedQuickUnionUFWPC(int N){
        count=N;
        id=new int[N];
        sz=new int[N];
        for(int i=0;i<N;i++){
            id[i]=i;
            sz[i]=1;
        }
    }
    
    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;
    }
    
    public void union(int p,int q){
        int pid=find(p);
        int qid=find(q);
        if(qid==pid)return ;
        if(sz[pid]<sz[qid]){
            id[pid]=qid;
            sz[qid]+=sz[pid];
        }
        else{
            id[qid]=pid;
            sz[pid]+=sz[qid];
        }
        count--;
    }
    
    public int count(){
        return count;
    }
    
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }
}

 

 

這四種方法能夠適應不同的情況,但是對於算法復雜度來說,這四種方法就會有很大的差別:

對於每一項的得出,《algorithm》給出了很詳細的解釋,我希望自己能夠有時間寫一篇文章來細講一下。(別說了。感覺還有好多坑沒填)


免責聲明!

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



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