用於不相交集合的數據操作——並查集

        假定有一組詞彙,其中有一些詞是同義詞,可以把意思不同的詞分別放到不同的集合中,構成一組不相交的集合,每個集合內部都是同義詞。最開始我們不知到哪些詞可以歸併到相同的組中,因此開始的時候它們每個詞爲一組。然後我們再一一給出哪些詞是同義詞,據此將初始的組進行合併……直到最後同義詞都被合併到各自應該歸屬的組裏面。

        講的簡單點,假定這些單詞是一個個的整數,他們構成了一組不相交的動態集合 S={S1,S2,,Sk},,每個集合可能包含一個或多個元素,並選出集合中的某個元素作爲代表。每個集合中具體包含了哪些元素是不關心的,具體選擇哪個元素作爲代表一般也是不關心的。我們關心的是,對於給定的元素,可以很快的找到這個元素所在的集合(的代表),以及合併兩個元素所在的集合,而且這些操作的時間複雜度都是常數級的。

        這就是並查集的問題,並查集的基本操作有三個:

  1. makeSet(s):建立一個新的並查集,其中包含 s 個單元素集合。
  2. unionSet(x, y):把元素 x 和元素 y 所在的集合合併,要求 x 和 y 所在的集合不相交,如果相交則不合並。
  3. 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;
	}
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章