7.2 等價類別和並查集
7.2.1 等價關係與等價類別
1、在求解實際應用問題時常會遇到等價類別的問題。
2、 從數學上看,等價類別是一個對象(或成員)的集合,在此集合中的所有對象應滿足等價關係。
3、 若用符號“≡”表示集合上的等價關係,那麼對於該集合中的任意對象x, y, z,下列性質成立:
(1)自反性:x ≡ x (即等於自身)。
(2)對稱性:若 x ≡ y, 則 y ≡ x。
(3)傳遞性:若 x ≡ y且 y ≡ z, 則 x ≡ z。
4、 因此,等價關係是集合上的一個自反、對稱、傳遞的關係。
5、“相等”(=)就是一種等價關係,它滿足上述的三個特性。
6、一個集合 S 中的所有對象可以通過等價關係劃分為若干個互不相交的子集 S1, S2, S3, …,它們的並就是 S。這些子集即為等價類別。
確定等價類別的方法 :(分兩步走)
第一步,讀入並儲存所有的等價對( i, j );
第二步,標記和輸出所有的等價類別。
void equivalence ( ) {
初始化;
while 等價對未處理完
{ 讀入下一個等價對 ( i, j );
儲存這個等價對 ; }
輸出初始化;
for ( 尚未輸出的每個對象 )
輸出包含這個對象的等價類別 ;
}
給定集合 S = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 },
及如下等價對: 0 ≡ 4, 3 ≡ 1, 6 ≡ 10, 8 ≡ 9, 7 ≡ 4, 6 ≡ 8, 3 ≡ 5, 2 ≡ 11, 11 ≡ 0
初始 {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}
0 ≡ 4 {0, 4}, {1}, {2}, {3}, {5}, {6}, {7}, {8}, {9}, {10}, {11}
3 ≡ 1 {0, 4}, {1, 3}, {2}, {5}, {6}, {7}, {8}, {9}, {10}, {11}
6 ≡ 10{0, 4}, {1, 3}, {2}, {5}, {6, 10}, {7}, {8}, {9}, {11}
8 ≡ 9 {0, 4}, {1, 3}, {2}, {5}, {6, 10}, {7}, {8, 9}, {11}
7 ≡ 4 {0, 4, 7}, {1, 3}, {2}, {5}, {6, 10}, {8, 9}, {11}
6 ≡ 8 {0, 4, 7}, {1, 3}, {2}, {5}, {6, 8, 9, 10}, {11}
3 ≡ 5 {0, 4, 7}, {1, 3, 5}, {2}, {6, 8, 9, 10}, {11}
2 ≡ 11{0, 4, 7}, {1, 3, 5}, {2, 11}, {6, 8, 9, 10}
11 ≡ 0{0, 2, 4, 7, 11}, {1, 3, 5}, {6, 8, 9, 10}
7.2.2 確定等價類別的鏈表方法
1、設等價對個數為m,對象個數為n。一種可選的儲存表示為單鏈表。
2、 可為集合的每一個對象建立一個帶表頭結點的單鏈表,並建立一個一維的指標數組 seq[n] 作為各單鏈表的表頭結點向量。 seq[i]是第 i 個單鏈表的表頭結點,第 i 個單鏈表中所有結點的data域存放的是在等價對中與 i 等價的對象編號。
3、當輸入了一個等價對( i, j )後,就將集合元素 i 鏈入第 j 個單鏈表,且將集合元素 j 鏈入第 i 個單鏈表。在輸出時,設定一個布爾型數組 out[n],用 out[i] 標記第 i 個單鏈表是否已經輸出。
void equivalence ( ) {
讀入 n;
將 seq 初始化為 0 且將 out 初始化為 False;
while等價對未處理完 {
讀入下一個等價對( i, j );
將 j 鏈入 seq[i]鏈表;
將 i 鏈入 seq[j]鏈表;
}
for ( i = 0; i < n; i++ ) //檢測所有對象
if ( out[i] == False ) { //若對象i未輸出
out[i] = True;
//對象i做輸出標誌輸出包含對象 i 的等價類別;
}
}
1、演算法的輸出從編號 i = 0 的對象開始,對所有的對象進行檢測。
2、在 i = 0 時,循第0個單鏈表先找出形式為( 0, j )的等價對,把 0 和 j 作為同一個等價類別輸出。再根據等價關係的傳遞性,找所有形式為( j, k )的等價對,把 k 也納入包含 0 的等價類別中輸出。如此繼續,直到包含 0 的等價類別完全輸出為止。
3、接下來再找一個未被標記的編號,如 i = 1,該對象將屬於一個新的等價類別,我們再用上述方法劃分、標記和輸出這個等價類別。
4、在演算法中使用了一個棧。每次輸出一個對象編號時,都要把這個編號進棧,記下以後還要檢測輸出的等價對象的單鏈表。
輸入所有等價對後的seq數組及各單鏈表的內容:
等價類別鏈表的定義
enum Boolean { False, True };
class ListNode { //定義鏈表結點類
friend void equivalence ( );
private:
int data; //結點資料
ListNode *link; //結點鏈指標
ListNode ( int d ) { data = d; link = NULL; }
};
typedef ListNode *ListNodePtr;
//建立等價類別演算法 (輸入等價對並輸出等價類別) 每當一個對象的單鏈表
//檢測完,就需要從棧中退出一個指標,以便繼續輸出等價類別中的其它對
//象。如果棧空,說明該等價類別所有對象編號都已輸出,再找一個使得
//out[i] == False的最小的i,標記並輸出下一個等價類別。
void equivalence ( ) {
ifstream inFile ( "equiv.in", ios::in ); //輸入檔案
if ( !inFile ) {
cout << “不能開啟輸入檔案" << endl;
exit (1);
}
int i, j, n;
inFile >> n; //讀入對象個數
seq = new ListNodePtr[n];
out = new Boolean[n]; //初始化seq和out
for (i = 0; i < n; i++) {
seq[i] = 0;
out[i] = False;
}
inFile >> i >> j; //輸入等價對 ( i, j )
while ( inFile.good ( ) ) { //輸入檔案結束轉出迴圈
x = new ListNode ( j ); //建立結點 j
x→link = seq[i];
seq[i] = x; //鏈入第i個鏈表
y = new ListNode ( i ); //建立結點i
y→link = seq[j];
seq[j] = y; //鏈入第j個鏈表
inFile >> i >> j; //輸入下一個等價對
}
for ( i =0; i <n; i++ ) if ( out[i] == False ) { //未輸出, 需要輸出
cout<< endl << “A new class: ” << i; //輸出
out[i] = True; //作輸出標記
ListNode *x = seq[i]; //取第i鏈表頭指標
ListNode *top = NULL; //棧初始化
while (1) { //找類的其它成員
while ( x ) { //處理鏈表,直到 x=0
j = x→data; //成員j
if ( out[j] == False ) { //未輸出, 輸出
cout << “,” << j;
out[j]=True;
ListNode *y = x→link;
x→link = top;
top = x; //結點x進棧
x = y; //x進到鏈表下一個結點
}
else x = x→link; //已輸出過,跳過
}
if ( top == NULL )
break; //棧空退出迴圈
else {
x = seq[top→data];
top = top→link;
}
//棧不空, 退棧, x是根據結點編號回溯的另一個鏈表的頭指標
}
}
delete [ ] seq;
delete [ ] out;
}
7.2.3 並查集
1、建立等價類別的另一種解決方案是先把每一個對象看作是一個單元素集合,
然後按一定順序將屬於同一等價類別的元素所在的集合合并。
2、在此過程中將反覆地使用一個搜尋運算,確定一個元素在哪一個集合中。
3、 能夠完成這種功能的集合就是並查集。它支援以下三種操作:
Union (Root1, Root2) //並操作;
Find (x) //搜尋操作;
UFSets (s) //建構函式。
4、一般情形,並查集主要涉及兩種資料類型:集合名類型和集合元素的類型。
5、對於並查集來說,每個集合用一棵樹表示。
6、 集合中每個元素的元素名分別存放在樹的結點中,
此外,樹的每一個結點還有一個指向其雙親結點的指標。
7、為此,需要有兩個映射:
集合元素到存放該元素名的樹結點間的對應;
集合名到表示該集合的樹的根結點間的對應。
8、設 S1= {0, 6, 7, 8 },S2= { 1, 4, 9 },S3= { 2, 3, 5 }
利用並查集來解決等價問題的步驟如下:
(1)利用UFSets操作, 建立UFSets型集合this, 集合中每一個元素初始化為0,各自形成一個單元素子集合, i =1, 2, …, n。n是集合中元素個數。
(2)重複以下步驟, 直到所有等價對讀入並處理完為止。 讀入一個等價對[i][j]; 用Find(i), Find(j)搜尋 i、j 所屬子集合的名 字x和y; 若x <> y. 用 Union(x,y) 或 Union(y,x) 將它們合并, 前者的根在 x;後者的根在 y。
(3)為簡化討論,忽略實際的集合名,僅用表示集合的樹的根來標識集合。
(4) 如果我們確定了元素 i 在根為 j 的樹中,而且j有一個指向集合名字表中第 k 項的指標,則集合名即為 name[k]。
(5) 為此,採用樹的雙親表示作為集合儲存表示。集合元素的編號從0到 n-1。其中 n 是最大元素個數。在雙親表示中,第 i 個數組元素代表包含集合元素 i 的樹結點。根結點的雙親為-1,表示集合中的元素個數。為了區別雙親指標資訊( >= 0 ),集合元素個數資訊用負數表示。
s1Us2的可能的表示方法:
並查集的類定義 :
const int DefaultSize = 10;
class UFSets { //並查集的類定義
public:
UFSets ( int s = DefaultSize );
~UFSets ( ) { delete [ ] parent; }
const UFSets & operator = ( UFSets const & Value );
void Union ( int Root1, int Root2 );
int Find ( int x );
void UnionByHeight ( int Root1, int Root2 );
private:
int *parent;
int size;
};
UFSets::UFSets ( int s ) { //建構函式
size = s;
parent = new int [size+1];
for ( int i = 0; i <= size; i++ )
parent[i] = -1;
}
unsigned int UFSets::Find ( int x ) { //搜尋操作
if ( parent[x] <= 0 )
return x;
else
return Find ( parent[x] );
}
void UFSets::Union ( int Root1, int Root2 ) { //並
parent[Root2] = Root1; //Root2指向Root1
}
Find和Union操作效能不好。假設最初 n 個元素構成 n 棵樹組成的森林,parent[i] = -1。
做處理Union(0, 1), Union(1, 2), …, Union(n-2, n-1)後,將產生的退化的樹。
執行一次Union操作所需時間是O(1), n-1次Union操作所需時間是O(n)。
若再執行Find(0), Find(1), …, Find(n-1),
若被搜尋的元素為i,完成Find(i)操作需要時間為O(i),完成 n 次搜尋需要的總時間將達到
退化的樹 :
Union操作的加權規則:
為避免產生退化的樹,改進方法是先判斷兩集合中元素的個數,如果以 i 為根的樹中的結點個數少於以 j 為根的樹中的結點個數,即parent[i] > parent[j],則讓 j 成為 i 的雙親,否則,讓i成為j的雙親。此即Union的加權規則。
void UFSets::WeightedUnion(int Root1, int Root2) { //按Union的加權規則改進的演算法
int temp = parent[Root1] + parent[Root2];
if ( parent[Root2] < parent[Root1] ) {
parent[Root1] = Root2; //Root2中結點數多
parent[Root2] = temp; //Root1指向Root2
}
else {
parent[Root2] = Root1; //Root1中結點數多
parent[Root1] = temp; //Root2指向Root1
}
}
使用加權規則得到的樹 :
使用並查集處理等價對,形成等價類別的過程:
Union操作的摺疊規則
為進一步改進樹的效能,可以使用如下的摺疊規則來“壓縮路徑”。即:如果 j 是從 i 到根的路徑上的一個結點,並且 parent[j] ≠ root[j], 則把 parent[j] 置為 root[i]。
int UFSets::CollapsingFind ( int i ) { //使用摺疊規則的搜尋演算法
for ( int j = i; parent[j] >= 0; j = parent[j]); //讓 j 循雙親指標走到根
while ( i != j ) {//換 parent[i] 到 j
int temp = parent[i];
parent[i] = j;
i = temp;
}
return j;
}
使用摺疊規則完成單個搜尋,所需時間大約增加一倍。但是,它能減少在最壞情況下完成一系列搜尋操作所需的時間。