並查集模板
技術標籤:並查集
文章目錄
1.並查集
(1)並查集定義
1.顧名思義,分為三個步驟——並(Union)、查(Find)、集(Set),即並查集支援【合併兩個集合】和【查詢】操作,查詢指判斷2個元素是否在一個集合中。
2.集合的判定——對於同一個結合來說只存在一個根結點,且將其作為所屬集合的標識。
3.int father[N]
,其中father[i]
標識元素i的父親結點,而父親結點本身也是這個集合內的元素,如father[1]=2
表示元素1的父親結點是元素2。
father[1]=1;
father[2]=1;
father[3]=2;
father[4]=2;
father[5]=5;
father[6]=5;
上面的定義即1、2、3、4為一個集合,其中元素1位該集合的根結點,而5和6為另外的一個集合,其中元素5是該集合的根結點。
4.若2個元素在相同的集合中,則不會對他們進行合併,從而保證在同一個集合中一定不會產生環,即並查集產生的每個集合都是一顆樹。
(2)第一步:初始化
初始時,每個元素都是獨立的一個集合,即for迴圈初始化所有的father[i]=i
。
for(int i=0;i<=N;i++){
father[i]=i;//令father[i]為-1也可以的
}
(3)第二步:查詢
//findFather函式返回元素x所在集合的根結點
int findFather(int x){
while(x!=father[x]){//如果不是根結點,繼續迴圈
x=father[x];//獲得自己的父親結點
}
return x;
}
如上圖中要查元素4的根結點是誰,
1.x=4,father[4]=2,不相同所以繼續查:獲得4的父親結點即2;
2.x=2,father[2]=1,不相同所以繼續查:獲得2的父親結點即1;
3.x=1,father[1]=1,相同即找到根結點,返回1.
如果用遞迴實現如下:
int findFather(int x){
if (x==father[x]) return x;//如果找到根結點,則返回根結點編號x
else return findFather(father[x]);//否則,遞迴判斷x的父親結點是否根結點
}
(4)合併
即將2個集合合併為1個集合——把一個集合的根結點的父結點指向另一個集合的根結點。
兩步走:
【1】利用findFather
函式找出各自根結點(是看否相同)從而判斷給定的2個元素a和b是否為同一集合。
【2】合併:把其中的一個集合的父結點faA指向另一個集合的父結點faB,即father[faA]=faB
。
void Union(int a,int b){
int faA=findFather(a);
int faB=findFather(b);
if(faA!=faB){//如果不屬於同一個集合
father[faA]=faB;
}
}
注意father[a]=b
不能實現合併,如上圖的father[4]=6或father[6]=4,則會得到下面的效果(不能實現集合的合併)。
2.路徑壓縮(優化)
並查集的路徑壓縮——把當前查詢結點的路徑上的所有結點的父親都指向根結點。
查詢就不需要一直回溯去找父結點了。
優化的過程:
(1)按原先的寫法獲得x的根結點r;
(2)重新從x開始走一遍尋找根結點的過程,把路徑上經過的所有結點的父親全部改為根結點r。
在查詢時把尋找根結點的路徑壓縮:
int findFather(int x){
//由於x在下面的while中會變成根結點,因此先把原先的x儲存一下
int a=x;
while(x!=father[x]){//尋找根結點
x=father[x];
}
//到這裡,x存放的是根結點,下面把路徑上的所有結點的father都改成根結點
while(a!=father[a]){
int z=a;//因為a要被father[a]覆蓋,所有先儲存a的值,以修改father[a]
a=father[a];//a回溯父結點
father[z]=x;//將原先的結點a的父親改為根結點x
}
return x;//返回根結點
}
可以把路徑壓縮後的並查集查詢函式均攤效率為O(1),
另外遞迴版本如下:
int findFather(int v){
if(v==father[v])
return v;
else{
int F=findFather(father[v]);//遞迴尋找father[v]的根結點F
father[v]=F;//將根結點F賦值給father[v]
return F;//返回根結點F
}
}
3.栗子
【好朋友】
有一個叫做“數碼世界”奇異空間,在數碼世界裡生活著許許多多的數碼寶貝,其中有些數碼寶貝之間可能是好朋友,並且數碼寶貝世界有兩條不成文的規定:
第一,數碼寶貝A和數碼寶貝B是好朋友等價於數碼寶貝B與數碼寶貝A是好朋友
第二,如果數碼寶貝A和數碼寶貝C是好朋友,而數碼寶貝B和數碼寶貝C也是好朋友,那麼A和B也是好朋友。
現在給出這些數碼寶貝中所有好朋友的資訊,問:可以把這些數碼寶貝分成多少組,滿足每組中的任意兩個數碼寶貝都是好朋友,而且任意兩組之間的數碼寶貝都不是好朋友。
輸入格式:輸入的第一行有2個正整數n和m(分別表示數碼寶貝的個數和好朋友的組數)
接下來有m行(每行有2個正整數a和b,表示數碼寶貝a和數碼寶貝b是好朋友)。
4 2
1 4
2 3
輸出格式:輸出一個這些數碼寶貝可以分成的組數。
2
(1)思路
在輸入這些好朋友關係時就同時對他們進行並查集的合併操作,處理後就能得到一些集合。
對同一個集合來說只存在一個根結點,且將其作為所屬集合的標識。
——開一個bool
型陣列falg[N]來記錄每個結點是否作為某個集合的根結點,這樣當處理完輸入資料後就能遍歷所有元素,令它所在集合的根結點的flag
值設為true,
最後累加flag陣列中的元素既可以得到集合數目。
(2)程式碼
#include<cstdio>
#include<iostream>
using namespace std;
const int N=110;
int father[N];//存放父結點
bool isRoot[N];//記錄每個結點是否作為某個集合的根結點
int findFather(int x){
//由於x在下面的while中會變成根結點,因此先把原先的x儲存一下
int a=x;
while(x!=father[x]){//尋找根結點
x=father[x];
}
//到這裡,x存放的是根結點,下面把路徑上的所有結點的father都改成根結點
//路徑壓縮可不寫
while(a!=father[a]){
int z=a;//因為a要被father[a]覆蓋,所有先儲存a的值,以修改father[a]
a=father[a];//a回溯父結點
father[z]=x;//將原先的結點a的父親改為根結點x
}
return x;//返回根結點
}
void Union(int a,int b){
int faA=findFather(a);
int faB=findFather(b);
if(faA!=faB){//如果不屬於同一個集合
father[faA]=faB;
}
}
void init(int n){
for(int i=1;i<=n;i++){
father[i]=i;
isRoot[i]=false;//一開始預設每個結點都非根結點
}
}
int main(){
int num,groupnum;
int a,b;//一組中的2個好朋友
scanf("%d%d",&num,&groupnum);
init(num);
//輸入兩個好朋友的關係(for迴圈依次合併)
for(int i=0;i<groupnum;i++){
scanf("%d%d",&a,&b);
Union(a,b);
}
//令所有結點的父結點為陣列下標的isRoot[]=1
for(int i=1;i<=num;i++){
isRoot[findFather(i)]=true;
}
int ans=0;
for(int i=1;i<=num;i++){
ans+=isRoot[i];
}
printf("%d\n",ans);
system("pause");
}
其他快方法:https://blog.csdn.net/DedicateToAI/article/details/103039223
PS:如果要求每個集合中元素的數目,則只要把isRoot陣列型別設為int即可。