Bootstrap

详解并查集

并查集

作用

并查集(Disjoint Set)是一种用来解决一些等价关系问题的数据结构,它主要用于处理一些不相交集合的合并和查询操作。

  1. 等价关系的判断:可以用并查集来判断两个元素是否属于同一个集合,从而判断它们之间是否存在某种等价关系。

  2. 连通性的判断:可以用并查集来判断图中两个节点是否连通,即是否存在路径可以从一个节点到达另一个节点。

  3. 集合的合并与划分:可以用并查集来合并两个集合或者将一个集合划分成多个子集。

  4. 判断图中是否有环。

先看模板😀

定义:

const int MAX=1e5+9;
int p[MAX];//p[i]表示编号为i的人的父亲是编号为p[i]的人。

MAX可以根据题目数据调整。

 查询一个点的祖先:

int find(int x){
	if(p[x]==x){
		return x;//如果当前位置的祖先是他本身,返回他 
	}
	return find(p[x]);//否则,就继续递归找下去 
}

合并两个点:

void merge(int xx,int y){
	int fx=find(x),fy=find(y);//分别找到两个点的祖先 
	if(fx!=fy){//如果祖先不相同,说明他俩不在一个集合中,进行合并。 
		p[fx]=fy;//或者p[fy]=fx,都可以。 
	}
} 

详细解释

并查集的主要是维护朋友 的朋友 的朋友 的朋友………………………………的朋友是朋友的关系。

举个例子

                     

 A,X,Y,Z,I,J,K分别表式他们的编号,A和X是朋友,和Y也是朋友,根据并查集的特性,X和Y也是朋友,类推下去,Z和K也是朋友。我们肉眼很容易辨认,但计算机如何实现呢?

#include<bits/stdc++.h>//万恶之源
using namespace std;
int n,m;
int x,y;
int p[1009];

首先我们定义n,m分别表示人数和关系数量。x,y表示x和y是朋友关系,p[] 就是并查集了。

int find(int x){
	if(p[x]==x){
		return x;
	}
	return find(p[x]);
}
void merge(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx!=fy){//如果在一个集合中,就不用合并了。
		p[fx]=fy;
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) p[i]=i;
	for(int i=1;i<=m;i++){
		cin>>x>>y;
		merge(x,y);
	}
	return 0;
}

先看主函数:

for(int i=1;i<=n;i++) p[i]=i;这个是什么意思呢?

据我所知,一个数组的所有元素的初始值都为0,根据p[i]表示的意思,即为并查集中所有元素的祖先都是0,再往下操作就没有意义了。所以要先把他们的祖先先设为自己,这个步骤叫初始化。

带上数据

我们先造一个数据 根据数据来解释

7 6  

1 2

1 3

2 4

2 5

3 6 

3 7

 在初始化完成之后,我们输入x和y,分别为 1 2。

进入 merge 函数之后分别找他们的祖先,分别为 1 2 。不相同,则连接到一块(p[1]=2)。

此时的p[]:

第1次             x=1  p[]   2  2  3  4567
y=2下标1234567

再次输入  x=1,y=3,同上步骤。进行操作完之后,p数组:

第2次x=1p[]数组2334567
y=3下标21234567

第2次:

find(x);👇
      1. p[1]!=1,所以继续find(p[1]),
      2. p[2]==2,所以return 2。

find(y);👇
      1. p[3]==3,所以return 3。

2!=3,所以p[2]=3。

第3次x=2p[]数组2344567
y=4下标1234567

第3次:

find(x);👇
       1. p[2]!=2,所以继续find(p[2]),
       2. p[3]==3,所以return 3。

find(y);👇 
       1. p[4]==4,所以return 4。

3!=4,所以p[3]=4。

第4次x=2p[]数组2345567
y=5下标1234567

第4次:

find(x);👇
       1. p[2]!=2,所以继续find(p[2]),
       2. p[3]!=3,所以继续find(p[3]),
       3. p[4]==4,所以return 4。

find(y);👇
       1. p[5]==5,所以return 5。

4!=5,所以p[4]=5。

第5次x=3p[]数组2345667
y=6下标1234567

第5次:

find(x);👇
       1. p[3]!=3,所以继续find(p[3]),
       2. p[4]!=4,所以继续find(p[4]),
       3. p[5]==5,所以return 5。

find(y);👇
       1. p[6]==6,所以return 6。

5!=6,所以p[5]=6。

第6次x=3p[]数组2345677
y=7下标1234567

第6次:

find(x);👇
       1. p[3]!=3,所以继续find(p[3]),
       2. p[4]!=4,所以继续find(p[4]),
       3. p[5]!=5,所以继续find(p[5]),
       4. p[6]==6,所以return 6。

find(y);👇
       1. p[7]==7,所以return 7。

6!=7,所以p[6]=7。

结合模板和数据自己画一下过程,理解会更加深刻。 

总过程可概括为:

       合并时,找到他们各自的祖先,合并他们的祖先,他们也就自然的在一个集合中啦。

———————————————————————————————————————————

如何得知两个人是否是朋友关系呢?

只要他在同一个集合里就一定是朋友了,而只要他俩的祖先相同,那他俩一定在同一个集合!

即:

if(find(人1)==find(人2)){
    是朋友;
}else{
    不是朋友;
}

使用并查集判断图中是否有环

 作为一个高级动物,我们一眼就能看出 1,3,4构成了一个环。

但电脑看不出来,只能由我们告诉他怎么找。

我们来跟据上图造数据:

pass:学会造数据是一项很重要的技能

6 6 (有6个节点,6条边)

1 2

1 3

1 4

2 6 

3 4

4 5

那就不得不提到好用的并查集了👇

求图中环个数的代码

#include<bits/stdc++.h>
using namespace std;
int n,m;
int x,y;
int p[1009];
int ans;
int find(int x){//
	return (p[x]==x)?x:find(p[x]);
}
void merge(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx==fy){
		ans++;//具体讲一下这里
	}
	p[fx]=fy;
}//模板
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		p[i]=i;
	} 
	for(int i=1;i<=m;i++){
		cin>>x>>y;
		merge(x,y);
	}
	cout<<ans;
	return 0;
}

详细解释

在一个图中如果两个节点连接的时候发现已经在一个集合中了,那么再连一条边,一定能构成一个环。请读者自己画图论证。

技巧

压缩路径

 如图,如果n小一些,find()函数还能承受,但是,如果n大一些,就不行了。这时候就要用到路径压缩。

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

直接让p[i]表示他的祖先,这样就会减少循环次数。

未压缩
n=18数组2345678910111213141516171818
下标123456789101112131415161718
 已压缩
n=18数组24681012141617111513151517171818
下标123456789101112131415161718

步骤明显减少。 

结语

关于作者对并的理解已经毫无保留了,据作者了解还有一种并查集叫种类并查集,他是用来维护敌人的敌人是朋友这种关系,感兴趣的读者可以自行搜索。

;