麻麻,我們班的孩子都分為好幾個幫派,我要怎么做才能知道他們總共分了有幾個幫派呀,我要怎么才能知道他們有沒有人同時在兩個幫派呀;
接下來就進入我們的並查集專題,英文名稱Union-Find。
並查集是一種用於不相交集合的數據結構,並查集通過幾個操作來建立,修改,查找和維護一些不相交的集合,一般用於不相交集合的合並問題,他也衍生出許多其他的應用,圖論中就會用並查集判環的操作,並查集還有着其他更為廣泛
的應用。
由於計算機經常會遇到數據量很大的情況,或者要反復查詢某個元素所屬的集合,這類問題並查集統統搞定。
首先我們介紹一下他的基本操作:
Init_set(v):建立集合操作,初始化每個元素單獨成為一個集合。
find(v):查找操作,我們用一個集合中其中一個元素的下標來表示這個集合,用二叉樹表示的話就是二叉樹的根,這個函數會返回v的最上層節點,即這個二叉集合數的根。
Join(x, y):合並操作,將包含x集合和y集合的兩個集合合並為一個新的集合。
is_same(u, v):查重操作,如果元素u和v在同一個集合中則返回true;否則返回false。
下面我們給出這些操作的基本代碼:
1 #include <cstdio> 2 using namespace std; 3 4 const int maxn = 1e6 + 5; 5 int n, head[maxn]; 6 7 void Init_set() { 8 for(int i = 1; i <= n; i ++) head[i] = i; 9 } 10 11 int find(int u) { 12 if(head[u] == u) return u; 13 return find(head[u]); 14 } 15 16 void Join(int x, int y) { 17 int fx = find(x), fy = find(y); 18 if(fx != fy) head[fx] = fy;//我們把y的根節點的父親點設為x的最上層結點就行了 19 } 20 21 int is_same(int u, int v) { 22 return (find(u) == find(v)); 23 }
下面給出一個並查集的樣例:
以5個結點為例,圓圈內為結點的標號,我們用head數組記錄每個結點的父親結點,head[i]表示i結點的父親節點的標號,當head[i] = i時,結點i的父節點是它本身,那么i的所在的樹的根就是i。
初始化,head[i] = i,他們分別獨立成為一個集合,分別作為自己的父節點。
Init:
head[ i ] = i;
合並1和2時,2的父節點變為1,head[2] = 1, 此時有
head[1] = 1; head[2] = 1, head[3] = 3, head[4] = 4, head[5] = 5;
如果我們現在分別查找1和2結點所在樹的根節點,1結點所在的樹的根節點為1,2結點所在樹的根節點也是1,也就是說1和2此時位於根結點為1
的同一顆樹內。
接下來我們合並1和3,讓head[3] = 1,即使得3的父節點為1,有
head[1] = 1; head[2] = 1, head[3] = 1, head[4] = 4, head[5] = 5;
很明顯能知道1,2,3的最頂層節點都是1。
我們在合並4和5,讓4的父節點變為5,即head[4] = 5;
最后我們合並5和3,讓5的父節點為3,即head[5] = 3。可以得出如下的圖。
在上述算法中很容易可以看出,每個結點都有一個根結點,而兩個集合合並時會以其中一個集合的根節點為新集合的根節點將另一個集合插入,即形成了一顆二叉樹。
觀察上述算法,可以發現基本所有操作都是基於find函數的,所以我們可以想可不可以有優化呢?我們發現初始化每個結點只進行一次,但是find和join都會進行多次,所以如果程序很大的話跑起來非常吃力。
下面我們就介紹兩種對該算法的優化:
路徑壓縮:這種策略非常的簡單高效。正如下面的代碼中,在find操作之后,使用這種策略會使得查找路徑中的每個結點直接指向根結點,路徑壓縮並不需要改變其余的任何東西。
1 int find(int u) {
2 if(head[u] == u) return u; 3 return head[u] = find(head[u]); 4 }
按秩合並:這種做法就是使具有較少結點的樹的根指向具有較多結點的樹的根。對於每個結點,我們維護一個rank值,通過比較rank值我們進行相應的操作。
1 #include <cstdio> 2 using namespace std; 3 4 const int maxn = 1e6 + 5; 5 int n, head[maxn], rank[maxn]; 6 7 void Init_set() { 8 for(int i = 1; i <= n; i ++) { 9 head[i] = i; 10 rank[i] = 0; 11 } 12 } 13 14 int find(int u) { 15 if(head[u] == u) return u; 16 return head[u] = find(head[u]); 17 } 18 19 void join(int x, int y) { 20 int fx = find(x), fy = find(y); 21 if(fx == fy) return; 22 if(rank[fx] > rank[fy]) 23 head[fy] = fx; 24 else { 25 head[fx] = fy; 26 if(rank[fx] == rank[fy]) rank[fy] += 1;//最重要的是修改祖先的rank,所以只需要修改祖先的rank,子孫的rank不用管 27 } 28 } 29 30 int is_same(int u, int v) { 31 return (find(u) == find(v)); 32 }
后續跟進並查集的相關題目......