不相交集合-並查集

並查集的定義

不相交集合維護了一個不相交動態集的集合。我們用一個代表來標示每個集合,而這個代表是這個集合的某個成員。
該集合中最主要的兩個操作Union(合併)與Find(查詢),因此該數據結構也叫做並查集。讓我們看一下百度百科的介紹:

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

可以看出我們只是想要查找某個元素所在的集合或者判斷兩個元素是否是屬於同一個集合,並沒有必要把查找的路徑返回,這也正是並查集的特點。

並查集的操作

根據並查集的特點,可以得到並查集的基本操作.

操作 說明
Make-Set(x) 建立一個新的集合,它的唯一成員是x
Union(x,y) 將包含x和y的兩個動態集合合併爲一個新的集合
Find-Set(x) 返回一個指針,這個指針指向包含x的集合的代表

在一個不相交集較快的實現方式中,我們使用有根樹來表示集合,樹中每個節點包含一個成員,每棵樹代表一個森林,從而多個集合構成了一個不相交集合森林。數中每個節點僅指向其父節點,如下圖(圖來自算法導論)。

這裏寫圖片描述

對於這樣的實現方式,每個操作的具體流程如下。

Make-Set

對於集合的存儲,我們可以選擇最簡單的數組,數組存儲每個元素的父節點。一開始創建沒個小集合時,該集合的父節點就是其本身。

int group[MAXN];
void make_set(int x)  
{   /*創建一個單元集*/  
     group[x] = x;  
} 

Union

上述圖中的最右邊的樹就是左邊兩顆樹合併之後的結果。這種情況下Union的操作是非常簡單的。

void Union(int x,int y)
{
    group[find(x)] = find(y);
}

合併兩個節點只需將一個節點的根節點的父親設定爲另外一個節點的根節點即可。

Find-Set

查找某個元素所屬的集合,即是查找該元素所在樹的根節點,因此最後經過若干次查找,一個節點總是能夠找到它的根節點,即滿足group[root] = root的節點也就是樹的根節點了。

int find(int x)  
{   
    // 尋找x節點所在組的根節點,根節點具有性質group[root] = root  
    while (x != group[x]) x = group[x];  
    return x;  
}  

按秩合併與路徑壓縮

按秩合併

在進行Union時,若能將較少節點的樹根指向節點較多的樹根,而不是反過來,這樣能夠讓樹看起來更加的平衡,並且更加有利於find操作。因此我們拿出額外的空間來存儲每個節點的秩,這個秩表示了該節點高度的一個上屆,Union操作時,可以讓較小高度的秩指向較大高度的秩。

路徑壓縮

不斷地Union之後,有可能出現樹的高度很高的極端情況,這樣Find操作的效率較低,使用路徑壓縮可以使查找路徑的每個節點直接指向根。如下圖:
這裏寫圖片描述
b圖是指向了Find-Set之後的的同一個集合,現在查找路徑上每個節點都指向了根。

那麼加入按秩合併和路徑壓縮之後的代碼如下:

int group[MAXN];    
int rank[MAXN];    /*rank[x]是x的高度的一個上界*/

void make_set(int x)
{   /*創建一個單元集*/
    group[x] = x;
    rank[x] = 0;
}

int find_set(int x)
{   /*帶路徑壓縮的查找*/
    if(x != group[x])
        group[x] = find_set(group[x]);
    return group[x];
}

/*按秩合併x,y所在的集合*/
void union_set(int x, int y)
{
    x = find_set(x);
    y = find_set(y);
    if(rank[x] > rank[y])/*讓rank比較高的作爲父結點*/
        group[y] = x;
    else 
    {
        group[x] = y;
        if(rank[x] == rank[y])
            rank[y]++;
    }
}

上述的Find-Set值得一提,其是一種兩趟方法,遞歸時沿着路徑找到根節點,回溯時,更新每個節點,使其指向根。

時間複雜度

使用了路徑壓縮和按秩合併之後,整個並查集的時間複雜能與總操作數m呈線性關係。最壞情況是O(mx(n)),x(n)是一個增長非常慢的函數。具體的證明請參考算法導論21.4節,過程並不簡單,這裏不再論述。

發佈了42 篇原創文章 · 獲贊 32 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章