並查集()


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

       並查集是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合並及查詢問題。常常在使用中以tree來表示。

用途
維護一個無向圖的連通性,判斷n個點m條邊時最少加多少邊可以連通所有點
判斷在一個無向圖中,兩點間加邊是否會產生環(最小生成樹克魯斯卡爾中有用到)
維護集合等操作
操作
(1)Union(Root1, Root2):把子集合Root2並入集合Root1中。要求這兩個集合互不相交,否則不執行合並。 
(2)Find(x):搜索單元素x所在的集合,並返回該集合的名字。 
(3)UnionFindSets(s):構造函數,將並查集中s個元素初始化為s個只有一個單元素的子集合。

初始化
用數組來建立一個並查集,數組下標代表元素,下標對應的值代表父節點,全部初始化為-1,根節點為一個集合的元素個數,數組的長度為並查集的初始連通分量的個數。並查集要求各集合是不相交的,因此要求x沒有在其他集合中出現過。算法如下:

//構造函數 

UF(int size){
    this->count = size;
    array = new int[size];
    for(int i = 0 ; i < size ; i++){
        this->array[i] = -1;
    }
}

 

查找操作
返回能代表x所在集合的節點,通常返回x所在集合的根節點。這里的查找操作通常采用路徑壓縮的辦法,即在查找過程中組不減小樹的高度,把元素逐步指向一開始的根節點。這樣下次再找根節點的時間復雜度會變成o(1)。如下圖所示 
 
算法如下:

//查找操作,路徑壓縮

int Find(int x){
    if(this->array[x] < 0){
        return x;
    }else{
    //首先查找x的父節點array[x],然后把根變成array[x],之后再返回根 
        return this->array[x] = Find(this->array[x]);
    }
}

 

並操作
將包含x,y的動態集合合並為一個新的集合。合並兩個集合的關鍵是找到兩個集合的根節點,如果兩個根節點相同則不用合並;如果不同,則需要合並。 
這里對並操作有兩種優化:根節點存樹高的相反數或者根節點存集合的個數的相反數,這兩種方法統稱按秩歸並。通常選用第二種方法。 
歸並過程如下圖: 
 
算法如下:

//並操作,跟結點存儲集合元素個數的負數
//通過對根結點的比較 

void Uion(int root1, int root2){
    root1 = this->Find(root1);
    root2 = this->Find(root2);
    if(root1 == root2){
        return;
    }else if(this->array[root1] < this->array[root2]){
        //root1所代表的集合的個數大於root2所代表集合的個數
        //因為為存放的是元素個數的負數 
        this->array[root1] += this->array[root2];
        this->array[root2] = root1;
        count--;
        }else{
            this->array[root2] += this->array[root1];
            this->array[root1] = root2;
            count--;
        }
    }
}

 

 

實現方案
1 樹結構(父指針表示法)

用這種實現方式,每個集合用一棵樹表示,樹的每一個節點代表集合的一個單元素。所有各個集合的全集合構成一個森林,並用樹與森林的父指針表示法來實現。其下標代表元素名。第I個數組元素代表包含集合元素I的樹節點。樹的根節點的下標代表集合名,根節點的父為-1,表示集合中元素個數。 
下面看一個例子: 
全集合是S = {0,1,2,3,4,5,6,7,8,9},初始化每個元素自成為一個單元素子集合。(書上原圖,感覺挺清晰的) 
 
經過一段時間的計算,這些子集合並成3個集合,他們是全集合S的子集合:S1 = {0,6,7,8},S2= {1,4,9},S3 = {2,3,5}。則表示他們並查集的樹形結構如下圖: 
 
上面數組中的元素值有兩種含義: 
(1)負數表示當前節點是樹的根節點,負數的絕對值表示樹中節點的個數,也即集合中元素的個數。 
(2)正數表示其所屬的樹的根節點,由樹形表示很容易理解,這也是樹的父指針表示的定義。 
經過上面對相關數據的組織,再回頭來看並查集的3中核心操作是怎樣依托於樹來實現的:

(1)將root2並入到root1中,其實就可以直接把root2的數組元素(就是他的父節點)改成root1的名字(就是他所在的數組下標)。 
下面的圖表示了合並兩個子集合的過程: 
 
(2)查找x所屬於的根節點(或者說是x所屬於的集合),就可以一直找array[x],直到array[x]小於0,則證明找到了根(所在集合)。 
下面的圖示意了查找一個節點所屬集合的過程: 
 
(3)將整個集合初始化為單元素集合,其實就是建立樹的父指針數組的過程,把數組元素全初始化為-1,也就表示了每個元素都各占一個集合。

有了上面的理論,代碼也比較容易實現出來!下面給出了一個代碼的實例:

/*
*樹結構構建並查集,其中樹用父指針形式表示
*/
#include <iostream>

const int DefaultSize = 10;

class UFSets {                                      //集合中的各個子集合互不相交
public:
    UFSets(int sz = DefaultSize);                   //構造函數       (並查集的基本操作)
    ~UFSets() { delete[] parent; }                 //析構函數
    UFSets& operator = (UFSets& R);                 //重載函數:集合賦值
    void Union(int Root1, int Root2);               //兩個子集合合並 (並查集的基本操作)
    int Find(int x);                                //搜尋x所在集合    (並查集的基本操作)
    void WeightedUnion(int Root1, int Root2);       //加權的合並算法 
private:
    int *parent;        //集合元素數組(父指針數組)
    int size;           //集合元素的數目
};

UFSets::UFSets(int sz) {
    //構造函數,sz是集合元素的個數,父指針數組的范圍0到sz-1
    size = sz;                                      //集合元素的個數
    parent = new int[size];                         //開辟父指針數組
    for (int i = 0; i < size; i ++) {               //初始化父指針數組
        parent[i] = -1;                             //每個自成單元素集合
    }
}

int UFSets::Find(int x) {
    //函數搜索並返回包含元素x的樹的根
    while (parent[x] >= 0) {
        x = parent[x];
    }
    return x;
}

void UFSets::Union(int Root1, int Root2) {
    //函數求兩個不相交集合的並,要求Root1與Root2是不同的,且表示了子集合的名字
    parent[Root1] += parent[Root2];                 //更新Root1的元素個數
    parent[Root2] = Root1;                          //令Root1作為Root2的父節點
}

void UFSets::WeightedUnion(int Root1, int Root2) {
    //使用節點個數探查方法求兩個UFSets集合的並
    int r1 = Find(Root1);                           //找到root1集合的根
    int r2 = Find(Root2);                           //找到root2集合的根
    if (r1 != r2) {                                 //兩個集合不屬於同一樹
        int temp = parent[r1] + parent[r2];         //計算總節點數
        if (parent[r2] < parent[r1]) {              //注意比較的是負數,越小元素越多,此處是r2元素多
            parent[r1] = r2;                        //r1作為r2的孩子 
            parent[r2] = temp;                      //更新r2的節點個數
        }
        else {
            parent[r2] = r1;                        //...
            parent[r1] = temp;                      //...
        }
    }
}

 

代碼的注釋比較詳盡,我就不在贅言。但是有一個注意點我已經寫在了下面!

當前並查集的改進! 
的確,有一個極端的狀況使得上面的樹實現的並查集性能低下!問題原因在於,這里沒有規定子集合並的順序,更確切的說是子集一直在向同一個方向依附: 
下面的圖片展示了當Union(0,1),Union(1,2),Union(2,3),Union(3,4)執行完后的樹的形狀。 
 
在這種極端情況下他編變成了一個單鏈表(退化的樹),這樣的話,用Find函數查找完所有的節點所歸屬的集合將會開銷的時間復雜度為:O(n^2)。 


免責聲明!

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



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