并查集(并茶几)
并查集(并茶幾)的應(yīng)用
一、What‘s that?
并查集是一種樹型的數(shù)據(jù)結(jié)構(gòu),用于處理一些不相交集合(Disjoint Sets)的合并及查詢問題。常常在使用中以森林來表示。 ——百度百科
二、How to uphold
0.我們的需求
一開始我們擁有一個(gè)集族(集合的集合),集族中所有集合不相交,每個(gè)集合有且僅有一個(gè)元素。
百度百科告訴我們 :
并查集至少需要支持兩種操作:
1.合并某兩個(gè)集合a,b。
2.詢問兩個(gè)元素x,y是否屬于同一集合。
1.可行的想法
我們能夠想到用樹的結(jié)構(gòu)存儲這樣一個(gè)集合,每一次的合并就相當(dāng)于將其中一棵樹的根的父親改為另一子樹的根的編號。
記錄一個(gè)值f[i]表示i的父親結(jié)點(diǎn)(當(dāng)i為根時(shí),將父親視為自己,也就是f[i]=i)。
所以我們有一個(gè)性質(zhì):若f[i]=i,則i為此樹的根。
初始時(shí)所有的點(diǎn)的f[i]=i。
2.初始化
void init(){ for (int i=1;i<=n;i++) f[i]=i; }3.查找樹根
那么如何尋找一個(gè)結(jié)點(diǎn)所在的樹的根呢?
Q:只要沿著f[i]一直向上跳到f[i]=i,此時(shí)的i結(jié)點(diǎn)便是樹根
維護(hù)合并
接下來維護(hù)集合的合并操作。
例如我們需要合并2和6所在的集合:
那么只需要將2子樹的根結(jié)點(diǎn)(1結(jié)點(diǎn)),與6子樹的根結(jié)點(diǎn)(5結(jié)點(diǎn))連接即可:
void Union(int x,int y) {int xx=find(x),yy=find(y); //找到兩樹的根 if (xx!=yy) { f[yy]=xx; } //如果兩樹的根不同,合并兩棵樹 }5.優(yōu)化原始并茶幾
這就是并查集的“初始版本”了。
但我們發(fā)現(xiàn)它每一次的詢問時(shí)間復(fù)雜度可能達(dá)到O(n)。
因?yàn)楫?dāng)數(shù)據(jù)退化為一條鏈的時(shí)候,每一次對鏈尾的find的次數(shù)是O(size)
a.按秩合并
所以這樣一個(gè)數(shù)據(jù)結(jié)構(gòu)在我們看來并不優(yōu),嘗試著優(yōu)化。
我們發(fā)現(xiàn)一棵較高的樹x與一棵較低的樹y合并時(shí),應(yīng)該從較高的樹向較低的樹連邊,使其合并。是為按秩合并。(f[y]=x)
這一優(yōu)化據(jù)說能將時(shí)間復(fù)雜度降低為O(logn)
b.路徑壓縮
還有一個(gè)更簡單且高效的優(yōu)化:路徑壓縮。
我們發(fā)現(xiàn)我們并不需要知道我們?nèi)绾窝刂鴉[i]向上跳,我們只需要找到最終的root就能夠完成任務(wù)了。所以我們把f[i]的定義改為記錄i的祖先結(jié)點(diǎn)的編號,在我們每一次find樹根的時(shí)候,更新f[i]的值為樹根,如此一來,當(dāng)我們再次find(i)時(shí),就能一步找到樹根的位置。
這樣路徑壓縮優(yōu)化過后,每一次find時(shí)間復(fù)雜度理論上為O(1),union的時(shí)間也是O(1),這樣我們就完成了一個(gè)能夠維護(hù)部分集合操作的優(yōu)秀數(shù)據(jù)結(jié)構(gòu)。
(一般情況下,路徑壓縮后的時(shí)間復(fù)雜度已經(jīng)能達(dá)到O(1),所以通常不會寫較為麻煩的按秩合并,so以下的所有代碼舍棄按秩合并的優(yōu)化。)
三、How to use it?
并查集是一個(gè)短小精悍的數(shù)據(jù)結(jié)構(gòu),因此在各類競賽中的出現(xiàn)頻率還是比較高的,并查集也有一些巧妙的思路與方法。
一般的并查集能夠維護(hù):
1.首先來一道裸題:親戚
問題描述
若某個(gè)家族人員過于龐大,要判斷兩個(gè)是否是親戚,確實(shí)還很不容易,現(xiàn)在給出某個(gè)親戚關(guān)系圖,求任意給出的兩個(gè)人是否具有親戚關(guān)系。
規(guī)定
x和y是親戚,y和z是親戚,那么x和z也是親戚。如果x,y是親戚,那么x的親戚都是y的親戚,y的親戚也都是x的親戚。
數(shù)據(jù)輸入:
第一行:三個(gè)整數(shù)n,m,p,(n<=5000,m<=5000,p<=5000),分別表示有n個(gè)人,m個(gè)親戚關(guān)系,詢問p對親戚關(guān)系。
以下m行:每行兩個(gè)數(shù)Mi,Mj,1<=Mi,Mj<=N,表示Ai和Bi具有親戚關(guān)系。
接下來p行:每行兩個(gè)數(shù)Pi,Pj,詢問Pi和Pj是否具有親戚關(guān)系。
數(shù)據(jù)輸出:
P行,每行一個(gè)’Yes’或’No’。表示第i個(gè)詢問的答案為“具有”或“不具有”親戚關(guān)系
樣例:
input
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
output
Yes
Yes
No
solution:
一題并查集裸題,只是為了演示整體代碼。明顯只是維護(hù)集合的合并,詢問結(jié)點(diǎn)連通性。
#include <vector> #include <list> #include <map> #include <set> #include <deque> #include <queue> #include <stack> #include <bitset> #include <algorithm> #include <functional> #include <numeric> #include <utility> #include <sstream> #include <iostream> #include <iomanip> #include <cstdio> #include <cmath> #include <cstdlib> #include <cctype> #include <string> #include <cstring> #include <ctime> #include <cassert> #include <string.h> //#include <unordered_set> //#include <unordered_map>#define MP(A,B) make_pair(A,B) #define PB(A) push_back(A) #define SIZE(A) ((int)A.size()) #define LEN(A) ((int)A.length()) #define FOR(i,a,b) for(int i=(a);i<(b);++i) #define fi first #define se secondusing namespace std;template<typename T>inline bool upmin(T &x,T y) { return y<x?x=y,1:0; } template<typename T>inline bool upmax(T &x,T y) { return x<y?x=y,1:0; }typedef long long ll; typedef unsigned long long ull; typedef long double lod; typedef pair<int,int> PR; typedef vector<int> VI;const lod eps=1e-11; const lod pi=acos(-1); const int oo=1<<30; const ll loo=1ll<<62; const int mods=1e9+7; const int INF=0x3f3f3f3f;//1061109567 const int MAXN=20005; /*--------------------------------------------------------------------*/ //請省略以上部分int f[MAXN]; inline int read() {int f=1,x=0; char c=getchar();while (c<'0'||c>'9') { if (c=='-') f=-1; c=getchar(); }while (c>='0'&&c<='9') { x=(x<<3)+(x<<1)+c-'0'; c=getchar(); }return x*f; } int find(int x){ return f[x]=(f[x]==x?x:find(f[x])); } void Union(int x,int y) {int xx=find(x),yy=find(y); if (xx!=yy) f[xx]=yy; } void init(int n){ for (int i=1;i<=n;i++) f[i]=i; } int main() {int n=read(),m=read(),Case=read();init(n);for (int i=1;i<=m;i++){int x=read(),y=read();Union(x,y);}while (Case--){int x=read(),y=read();if (find(x)==find(y)) puts("Yes");else puts("No");}return 0; }2.一道稍稍不同的題:POJ1988 Cube Staking
Description
Farmer John and Betsy are playing a game with N (1 <= N <= 30,000)identical cubes labeled 1 through N. They start with N stacks, each containing a single cube. Farmer John asks Betsy to perform P (1<= P <= 100,000) operation. There are two types of operations:
moves and counts.
- In a move operation, Farmer John asks Bessie to move the stack containing cube X on top of the stack containing cube Y.
- In a count operation, Farmer John asks Bessie to count the number of cubes on the stack with cube X that are under the cube X and report that value.
Write a program that can verify the results of the game.
Input
- Line 1: A single integer, P
- Lines 2…P+1: Each of these lines describes a legal operation. Line 2 describes the first operation, etc. Each line begins with a ‘M’ for a move operation or a ‘C’ for a count operation. For move operations, the line also contains two integers: X and Y.For count operations, the line also contains a single integer: X.
Note that the value for N does not appear in the input file. No move operation will request a move a stack onto itself.
Output
Print the output from each of the count operations in the same order as the input file.
Sample Input
6
M 1 6
C 1
M 2 4
M 2 6
C 3
C 4
Sample Output
1
0
2
Source
USACO 2004 U S Open
Solution
題意:
有兩種操作:
這一題需要維護(hù)兩個(gè)額外的信息:
只需要稍稍更改find的過程即可。
#include <vector> #include <list> #include <map> #include <set> #include <deque> #include <queue> #include <stack> #include <bitset> #include <algorithm> #include <functional> #include <numeric> #include <utility> #include <sstream> #include <iostream> #include <iomanip> #include <cstdio> #include <cmath> #include <cstdlib> #include <cctype> #include <string> #include <cstring> #include <ctime> #include <cassert> #include <string.h> //#include <unordered_set> //#include <unordered_map>#define MP(A,B) make_pair(A,B) #define PB(A) push_back(A) #define SIZE(A) ((int)A.size()) #define LEN(A) ((int)A.length()) #define FOR(i,a,b) for(int i=(a);i<(b);++i) #define fi first #define se secondusing namespace std;template<typename T>inline bool upmin(T &x,T y) { return y<x?x=y,1:0; } template<typename T>inline bool upmax(T &x,T y) { return x<y?x=y,1:0; }typedef long long ll; typedef unsigned long long ull; typedef long double lod; typedef pair<int,int> PR; typedef vector<int> VI;const lod eps=1e-11; const lod pi=acos(-1); const int oo=1<<30; const ll loo=1ll<<62; const int mods=1e9+7; const int INF=0x3f3f3f3f;//1061109567 const int MAXN=30005; /*--------------------------------------------------------------------*/ //請省略以上部分int f[MAXN<<1],r[MAXN<<1],num[MAXN<<1]; inline int read() {int f=1,x=0; char c=getchar();while (c<'0'||c>'9') { if (c=='-') f=-1; c=getchar(); }while (c>='0'&&c<='9') { x=(x<<3)+(x<<1)+c-'0'; c=getchar(); }return x*f; } inline char readc() {char c=getchar();while (!isalnum(c)) c=getchar();return c; } inline int find(int x) {if (f[x]==x) return x;int tmp=f[x];f[x]=find(f[x]);r[x]+=r[tmp];return f[x]; } int main() {int Case=read();for (int i=1;i<=MAXN;i++) f[i]=i,num[i]=1,r[i]=0;for (int i=1;i<=Case;i++){char c=readc();if (c=='M'){int x=read(),y=read();int xx=find(x),yy=find(y);if (xx!=yy){f[yy]=xx;r[yy]=num[xx];num[xx]+=num[yy];}}else {int x=read();int xx=find(x);printf("%d\n",num[xx]-r[x]-1);}} return 0; }本文總結(jié)
以上是一些并查集的簡單應(yīng)用,但也是一些精妙并查集技巧操作的基礎(chǔ)。
若已經(jīng)熟練掌握這些基礎(chǔ),那么可以學(xué)習(xí)一些其他更精妙并查集的方法:
這些內(nèi)容以后博主會慢慢補(bǔ)充。
總結(jié)
- 上一篇: 笋的功效与作用、禁忌和食用方法
- 下一篇: 卷积与莫比乌斯反演