假定有一組詞彙,其中有一些詞是同義詞,可以把意思不同的詞分別放到不同的集合中,構成一組不相交的集合,每個集合內部都是同義詞。最開始我們不知到哪些詞可以歸併到相同的組中,因此開始的時候它們每個詞爲一組。然後我們再一一給出哪些詞是同義詞,據此將初始的組進行合併……直到最後同義詞都被合併到各自應該歸屬的組裏面。
講的簡單點,假定這些單詞是一個個的整數,他們構成了一組不相交的動態集合 S={S1,S2,⋯,Sk},,每個集合可能包含一個或多個元素,並選出集合中的某個元素作爲代表。每個集合中具體包含了哪些元素是不關心的,具體選擇哪個元素作爲代表一般也是不關心的。我們關心的是,對於給定的元素,可以很快的找到這個元素所在的集合(的代表),以及合併兩個元素所在的集合,而且這些操作的時間複雜度都是常數級的。
這就是並查集的問題,並查集的基本操作有三個:
- makeSet(s):建立一個新的並查集,其中包含 s 個單元素集合。
- unionSet(x, y):把元素 x 和元素 y 所在的集合合併,要求 x 和 y 所在的集合不相交,如果相交則不合並。
- find(x):找到元素 x 所在的集合的代表,該操作也可以用於判斷兩個元素是否位於同一個集合,只要將它們各自的代表比較一下就可以了。
1.並查集的森林表示
並查集的實現原理也比較簡單,就是使用樹來表示集合,樹的每個節點就表示集合中的一個元素,樹根對應的元素就是該集合的代表,如圖 1 所示。
圖 1 並查集的樹表示
圖中有兩棵樹,分別對應兩個集合,其中第一個集合爲 {a,b,c,d},代表元素是a;第二個集合爲{e,f,g},代表元素是e。
樹的節點表示集合中的元素,指針表示指向父節點的指針,根節點的指針指向自己,表示其沒有父節點。沿着每個節點的父節點不斷向上查找,最終就可以找到該樹的根節點,即該集合的代表元素。
2.構造並查集並初始化
現在,假設使用一個足夠大的的連續空間來存儲樹節點,那麼 makeSet 要做的就是構造出如圖 2 的森林,其中每個元素都是一個單元素集合,即父節點是其自身:
圖 2 構造並查集初始化
3.並查集的find操作
接下來,就是 find 操作了,如果每次都沿着父節點向上查找,那時間複雜度就是樹的高度,完全不可能達到常數級。這裏需要應用一種非常簡單而有效的策略——路徑壓縮。路徑壓縮,就是在每次查找時,令查找路徑上的每個節點都直接指向根節點,如圖 3 所示。
圖 3 路徑壓縮
4.並查集的合併操作
最後是合併操作 unionSet,並查集的合併也非常簡單,就是將一個集合的樹根指向另一個集合的樹根,如圖 4 所示。
圖 4 並查集的合併
這裏也可以應用一個簡單的啓發式策略——按秩合併。該方法使用秩來表示樹高度的上界,在合併時,總是將具有較小秩的樹根指向具有較大秩的樹根。簡單的說,就是總是將比較矮的樹作爲子樹,添加到較高的樹中。爲了保存秩,需要爲節點增加一個成員rank,並將其值初始化爲 0。(樹的秩:從樹的根節點x到某一後代葉節點的最長簡單路徑上邊的數目)
5.源代碼
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <iomanip>
using namespace std;
const int MAX = 20; //節點總數
const int NUM = 50; //操作次數
//節點定義
class Node{
public:
int num; //節點編號,可以用來代表單詞
int son_num; //該節點的孩子數量,代表本單詞同義詞家族單詞數目
int rank; //節點所在樹的秩
Node *father; //該節點家族的代表詞彙
public:
Node() : num(0), son_num(1){
father = NULL;
}
};
//並查集類操作定義
class UF{
public:
Node *s; //指向節點隊列的指針,用於分配和釋放內存
public:
UF();
~UF();
Node* find(Node *r); //查找本家族的代表詞彙
void unit(Node *x, Node *y); //合併家族詞彙
void print(); //打印所有的節點信息
};
//構造函數,批量申請內存並初始化
UF::UF(){
s = new Node[MAX]();
for(Node *p = s ; p < s + MAX; p++){
p -> num = p - s;
p -> son_num = 1;
p -> father = p;
p -> rank = 0;
}
}
//析構函數,釋放空間
UF::~UF(){
delete [] s;
s = NULL;
}
//查找本家族代表單詞
Node* UF::find(Node *r){
if(r == r -> father){
return r;
} else {
r -> father = find(r -> father);
return r -> father;
}
}
//合併兩個家族
void UF::unit(Node *x, Node *y){
Node *x_parent = find(x);
Node *y_parent = find(y);
if(x_parent != y_parent){
if(x_parent -> rank > y_parent -> rank){
y_parent -> father = x_parent;
x_parent -> son_num += y_parent -> son_num;
} else {
x_parent -> father = y_parent;
y_parent -> son_num += x_parent -> son_num;
if(x_parent -> rank == y_parent -> rank){
y_parent -> rank += 1;
}
}
}
}
//打印所有節點的信息,以供參考(當MAX大於20時建議不要用該函數)
void UF::print(){
cout << "序號:";
for(Node* p = s; p < s + MAX; p++){
cout << right << setw(3) << p -> num;
}
cout << endl;
cout << "祖先:";
for(Node* p = s; p < s + MAX; p++){
cout << right << setw(3) << p -> father -> num;
}
cout << endl;
cout << "大小:";
for(Node* p = s; p < s + MAX; p++){
cout << right << setw(3) << p -> son_num;
}
cout << endl;
cout << "秩: ";
for(Node* p = s; p < s + MAX; p++){
cout << right << setw(3) << p -> rank;
}
cout << endl;
}
//測試函數
int main(){
UF uf;
for(int i = 0; i < NUM; i++){
int a = rand()%MAX;
int b = rand()%MAX;
uf.unit(uf.s + a, uf.s + b);
cout << "第" << i << "次操作,a = " << a << " b = " << b << endl;
uf.print();
cout << endl;
}
}