[並查集]的基本操做

【摘要】

在一些有N個元素的集合應用問題中,咱們一般是在開始時讓每一個元素構成一個單元素的集合,而後按必定順序將屬於同一組的元素所在的集合合併,其間要反覆查找一個元素在哪一個集合中,這就是並查集思想。c++

這一類問題近幾年來反覆出如今信息學的國際國內賽題中,其特色是看似並不複雜,但數據量極大,若用圖的數據結構來表示關係的話過於「奢侈」了,每每在空間上過大,計算機沒法承受;即便在空間上勉強經過,運行的時間複雜度也極高,根本就不可能在比賽規定的運行時間(1~3秒)內計算出試題須要的結果,這就是爲何常考的緣由。其實這只是一個對分離集合(並查集)操做的問題。而並查集經過樹形結構,將沒必要要的計算去除,減小了連通圖的空間,優化了時間,達到了更高的效率。算法

【關鍵詞】樹型結構、並集、查集、優化數組

【並查集定義】並查集是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題。經常在使用中以森林來表示。數據結構

【關鍵操做】函數

初始化:把每一個點所在集合初始化爲其自身。優化

(這一步必不可少,是最關鍵的操做之一,這會直接影響到整串代碼的正確)spa

查找:查找元素所在的集合,即根節點。blog

(用遞歸或循環不停查找當前節點的父節點,直到該節點的父節點等於本身爲止)遞歸

合併:將兩個元素所在的集合合併爲一個集合。ci

(一般來講,合併以前,應先判斷兩個元素是否屬於同一集合,這可用上面的查找操做實現。)

【關鍵代碼】(c++語言)

初始化:

for(i=1;i<=n;i++)fath[i]=i;//將本身的父節點賦值爲自己

查找/判斷集合:

遞歸:

int find(int x){
if(fath[x]!=x){
    return find(fath[x]);
}
return x;
}

非遞歸:

int find(int x){
while(fath[x]!=x){
    x=fath[x];
}
return x;
}

  

【例題】親戚

問題描述:

或許你並不知道,你的某個朋友是你的親戚。他多是你的曾祖父的外公的女婿的外甥女的表姐的孫子。若是能獲得完整的家譜,判斷兩我的是否親戚應該是可行的,但若是兩我的的最近公共祖先與他們相隔好幾代,使得家譜十分龐大,那麼檢驗親戚關係實非人力所能及。在這種狀況下,最好的幫手就是計算機。爲了將問題簡化,你將獲得一些親戚關係的信息,如Marry和Tom是親戚,Tom和Ben是親戚,等等。從這些信息中,你能夠推出Marry和Ben是親戚。請寫一個程序,對於咱們的關於親戚關係的提問,以最快的速度給出答案。

輸入格式:

輸入由兩部分組成。

第一部分以N,M,Q開始。N爲問題涉及的人的個數(1≤N≤5000)。這些人的編號爲1,2,3,…, N。下面有M行(1≤M≤5000),每行有兩個數ai, bi,表示已知ai和bi是親戚。

第二部分如下Q行有Q個詢問(1≤Q≤5000),每行爲ci, di,表示詢問ci和di是否爲親戚。

輸出格式:

對於每一個詢問ci, di,輸出一行:若ci和di爲親戚,則輸出「Yes」,不然輸出「No」。

輸入樣例:

  10 7 3

  2 4

  5 7

  1 3

  8 9

  1 2

  5 6

  2 3

  3 4

  7 10

  8 9

輸出樣例:

  Yes

  No

Yes

【題目解析】

若是使用常規思路就是連通圖的算法,用Floyed算法經過第三路徑連通連兩個節點(親戚)再查找的確可行,但本題因時間空間要求對於它來講太嚴格,用連通圖的思想必定會超過期空限制,須要尋找新的思路。咱們能夠給每一個人創建一個集合,集合的元素值有他本身,表示最開始時他不知道任何人是它的親戚。之後每次給出一個親戚關係a, b,則a和他的親戚與b和他的親戚就互爲親戚了,將a所在集合與b所在集合合併。對於樣例數據的操做全過程以下:

輸入關係 分離集合

初始狀態

(2,4) {1} {2,4} {3} {5} {6} {7} {8} {9}

(5,7) {1} {2,4} {3} {5,7} {6} {8} {9}

(1,3) {1,3} {2,4} {5,7} {6} {8} {9}

(8,9) {1,3} {2,4} {5,7} {6} {8,9}

(1,2) {1,2,3,4} {5,7} {6} {8,9}

(5,6) {1,2,3,4} {5,6,7} {8,9}

(2,3) {1,2,3,4} {5,6,7} {8,9}

最後咱們獲得3個集合{1,2,3,4}, {5,6,7}, {8,9},因而判斷兩我的是否親戚的問題就變成判斷兩個數是否在同一個集合中的問題。如此一來,須要的數據結構就沒有圖結構那樣龐大了。

【並查集實現】

#include<cstdio>
#include<cstring>
using namespace std;
int n,m,p,i,t1,t2,fath[5001];
bool ans[5001];                     //用於儲存答案
int find(int x){
    if(fath[x]!=x)return find(fath[x]){  //若是x的父節點不是它自己就繼續找根節點
    	fath[x]=find(fath[x]);
	}
	return fath[x];                  //若是是就返回根節點的值
}
int main(){
    scanf("%d%d%d",&n,&m,&p);
    memset(ans,0,p);              //初始化答案
    for(i=1;i<=n;i++){             //初始化,使一個集合根節點爲自己
    	fath[i]=i;
	}
    for(i=1;i<=m;i++){
        scanf("%d%d",&t1,&t2);
        t1=find(t1);
        t2=find(t2);
        if(t1!=t2){                 //若是不在同一集合中就合併集合
        	fath[t2]=t1;
		}
    }
    for(i=1;i<=p;i++){
        scanf("%d%d",&t1,&t2);
        if(find(t1)==find(t2)){     //查找根節點
        	ans[i]=1;             //記錄答案
		}
    }
    for(i=1;i<=p;i++){            //輸出答案
    	if(ans[i])printf("Yes\n");
		else printf("No\n");
	}
    return 0;
}

  

思路解析:

並查集的思想,就是親戚就合併集合(樹),對於兩個親戚關係的人的集合進行合併的操做,將一我的的所屬樹掛在另外一我的所屬的樹下面,而後對於兩個判斷的人就找他們的根節點的人是不是同一個ok了。雖然得出的是正確答案,但這依然超過了時空限制。所以算法須要再優化,以下程序。

【並查集優化】

#include<cstdio>
#include<cstring>
using namespace std;
int n,m,p,i,t1,t2,fath[5001];
bool ans[5001];                     //用於儲存答案
int find(int x){                       //路徑優壓縮優化
    if(fath[x]!=x){                   //若是x的父節點不是它自己就繼續找根節點
    	fath[x]=find(fath[x]);
	}
	return fath[x];                  //若是是就返回根節點的值
}
int main(){
    scanf("%d%d%d",&n,&m,&p);
    memset(ans,0,p);              //初始化答案
    for(i=1;i<=n;i++){             //初始化,使一個集合根節點爲自己
    	fath[i]=i;
	}
    for(i=1;i<=m;i++){
        scanf("%d%d",&t1,&t2);
        t1=find(t1);
        t2=find(t2);
        if(t1!=t2){                 //若是不在同一集合中就合併集合
        	fath[t2]=t1;
		}
    }
    for(i=1;i<=p;i++){
        scanf("%d%d",&t1,&t2);
        if(find(t1)==find(t2)){     //查找根節點
        	ans[i]=1;             //記錄答案
		}
    }
    for(i=1;i<=p;i++){            //輸出答案
    	if(ans[i]){
    		printf("Yes\n");
		}
		else printf("No\n");
	}
    return 0;
}

  

思路解析:

對於以前的程序,此處最大的優化就是在尋找根節點的函數上,函數直接在遞歸過程當中順便將其合併的集合的子結點直接指向了根節點,這樣的路徑壓縮很是簡單而有效,能夠減小查找時的遞歸層數,大大減小了計算的時間。

【優化關鍵代碼】

 遞歸:

int find(int x){
if(fath[x]!=x){//若是不是根節點就繼續尋找
    fath[x]=find(fath[x]);//將子節點直接指向根節點
}
return x;//若是已尋找到根節點就返回根節點
}

  

非遞歸:

int find(int x){
int father,now=x,t=x;
while(fath[now]!=now){//尋找到根節點
    now=fath[now];
}
father=fath[now];//儲存根節點
while(fath[x]!=x){//將遍歷到的元素直接指向它的根節點
    x=fath[x];
    fath[t]=father;
    t=x;
}
return father;//返回根節點的值
}

  

對於這種優化思路,另外一種方式也能實現,以下。

【以另外一種方式優化】

#include<stdio.h>
using namespace std;
int n,m,q,i,t1,t2,fath[5001];
bool ans[5001];                          //用於儲存答案
void change(int a){                       //合併兩棵樹
	int i;
	for(i=1;i<=n;i++){                    //查找每個節點
		if(fath[i]==a&&fath[i]!=fath[t1]){
			fath[i]=fath[t1];               //合併節點
		}
	}
	return ;
}
int main(){
	scanf("%d%d%d",&n,&m,&q);
	for(i=1;i<=n;i++){                    //初始化集合
		fath[i]=i;
	}
	for(i=1;i<=m;i++){
		scanf("%d%d",&t1,&t2);
		if(fath[t1]!=fath[t2]){
			change(fath[t2]);             //並集
			fath[fath[t2]]=fath[t1];
		}
	}
	for(i=1;i<=q;i++){
		scanf("%d%d",&t1,&t2);
		if(fath[t1]==fath[t2]){            //只需看它們的父節點是否相同就好了
			ans[i]=1;                    //儲存答案
		}
		else ans[i]=0;
	}
	for(i=1;i<=q;i++){                   //輸出答案
		if(ans[i])printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

  

思路解析:

本思路就是將集合當作只有一層的樹,每次輸入兩個親戚的關係後就將一棵樹合併到另外一棵,此處就是直接將一棵樹直接指向另外一棵的根節點,這樣每個葉節點的父節點就是它所屬樹的根節點,查找起來十分方便,雖然沒有【並查集優化快】,但也減小了查找的時間。這種思路能夠再優化,就是將每一棵樹的子節點數量儲存下來,這樣在並集的時候就能減小時間,以下。

【以另外一種方式優化的優化】

  

#include<stdio.h>
using namespace std;
int n,m,q,i,t1,t2,fath[5001],num[5001];       //num數組用於記錄每一集合的成員個數
bool ans[5001];                               //用於儲存答案
void change(int a){
	int i;
	for(i=1;i<=n;i++){
		if(fath[i]==a&&fath[i]!=fath[t1]){      //合併集合
			fath[i]=fath[t1];
		}
	}
	return ;
}
int main(){
	scanf("%d%d%d",&n,&m,&q);
	for(i=1;i<=n;i++){                       //初始化元素的根節點
		fath[i]=i;
		num[i]=1;
	}
	for(i=1;i<=m;i++){
		scanf("%d%d",&t1,&t2);
		if(fath[t1]!=fath[t2]){
			if(num[fath[t1]]<num[fath[t2]]){  //判斷集合成員個數多少
				t1=t1^t2;t2=t1^t2;t1=t1^t2; //位運算交換兩個數的值
			}                               //等同於swap(t1,t2)
			num[fath[t1]]+=num[fath[t2]];   //合併後的成員個數
			change(fath[t2]);
			fath[fath[t2]]=fath[t1];
		}
	}
	for(i=1;i<=q;i++){
		scanf("%d%d",&t1,&t2);
		if(fath[t1]==fath[t2]){               //收集答案
			ans[i]=1;
		}
		else ans[i]=0;
	}
	for(i=1;i<=q;i++){                       //輸出答案
		if(ans[i]){
			printf("Yes\n");
		}
		else printf("No\n");
	}
	return 0;
}

  

思路解析:

本思路就是將集合中成員個數記錄下來,在輸入兩個成員關係時,將成員所屬集合成員少的合併到集合成員較多的集合中去,這樣在並集時能夠優化計算次數,但因爲增長了交換的步驟使其抵消了原來的時間,反而時間複雜度增多了,下面提供一種本優化思路時間複雜度最低的也是最快的一個程序。

  

#include<stdio.h>
using namespace std;
int n,m,q,i,t1,t2,fath[5001];
bool ans[5001];                          //用於儲存答案
void change(int a){                       //合併兩棵樹
	int i;
	for(i=1;i<=n;i++){                    //查找每個節點
		if(fath[i]==a&&fath[i]!=fath[t1]){
			fath[i]=fath[t1];               //合併節點
		}
	}
	return ;
}
int main(){
	scanf("%d%d%d",&n,&m,&q);
	for(i=1;i<=n;i++){                    //初始化元素的根節點
		fath[i]=i;
	}
	for(i=1;i<=m;i++){
		scanf("%d%d",&t1,&t2);
		if(fath[t1]!=fath[t2]){
			if(fath[t1]==t1&&fath[t2]!=t2){//僅判斷是否有本身的集合
				t1=t1^t2;t2=t1^t2;t1=t1^t2;
			}
			change(fath[t2]);             //並集
			fath[fath[t2]]=fath[t1];
		}
	}
	for(i=1;i<=q;i++){
		scanf("%d%d",&t1,&t2);
		if(fath[t1]==fath[t2])ans[i]=1     //只需它們的父節點是否相同再儲存答案
		else ans[i]=0;
	}
	for(i=1;i<=q;i++){                   //輸出答案
		if(ans[i])	printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

  

【總結】

並查集就是在一些有N個元素的集合應用問題中,輸入某兩個元素在一個集合中,合併集合,並對於輸入的有兩個元素的詢問判斷它們是否在一個集合中的問題。

若是這種關係用圖來表示空間和時間就都必定會超過限制,可是用樹形結構表示時空複雜度就要簡化不少,一開始將每個元素斷定在本身的集合中,而後對於輸入數據合併集合(如【並查集實現】)。信息學奧賽中給的時空限制都很苛刻,通常都須要路徑壓縮優化來減化時空才能經過(如【並查集優化】),這就須要在合併時直接將一個集合的全部結點直接指向另外一個集合的根節點,這樣查找時就能夠減小遞歸層數,從而作到優化時間和空間。

若是用【以另外一種方式優化】作這道題,雖然經過了提交,但這樣在合併集合時仍然有點費時間,應此改進後的思路【並查集優化】,就是在在尋找根節點時順便就把子節點指向了根節點,雖然不是全部的子節點都指向了,但的確是最優解。

相關文章
相關標籤/搜索