1. 程式人生 > 其它 >並查集模板

並查集模板

技術標籤:並查集

文章目錄

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即可。