1、并查集概念
根据百度百科的权威解释,并查集的定义大概如下所示:
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。 这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
2、文件与并查集的联系
(1)组合模式
说到树形结构,计算机里有一样众人皆知的东西——文件,其实就可以用树形结构来表示。
如果你上过设计模式这门课,GOF常用的23种设计模式中结构型模式类别中的组合模式,其中有一个经典的例子讲的就是文件。
那什么是组合模式呢,我们先来看看它的定义:
组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。
发觉了没,这段定义里也有树形结构的关键字!并且它还有拓展,它说组合模式使得单个对象和组合对象的使用具有一致性。
那这里的“一致性”指的是什么呢,我们不妨来类比一下文件夹,比如,在下面这张图里,我在桌面上新建了一个文件夹,名称为Boss
我们现在单从这个文件夹来看的话,它可以看作是单个对象,因为这个文件夹是我刚创建的,里面啥也没有。
接下来我再在Boss文件夹下再创建一个文件夹,名字为boss:
现在我们再来看,Boss就不再是单个对象了,因为Boss文件夹下有子文件夹boss,所以Boss应该被看作是组合对象,而子文件夹boss是我们刚创建的,里面啥都没,所以它则因被看作是单个对象。
因而现在的情况是:
单个对象 --》 boss文件夹
组合对象 --》Boss文件夹
现在我们再来看这句话,“组合模式使得单个对象和组合对象的使用具有一致性”,现在我们这里单个对象是文件夹,组合对象也是文件夹,所以它们使用起来时,很显然,都是一样的方法,因而具有所谓的一致性。
(2)文件夹查询
对于文件夹的查询,想必我们并不陌生吧?
对文件夹的查询常采用递归查询的方式。
比如现在我桌面上有一个文件夹1,文件夹1里有文件夹2,文件夹2里有文件夹3,文件夹3里有文件夹4,文件夹4里有文件夹5,因而针对文件夹5所在位置的相对路径是1->2->3->4
我如果想找到文件夹5,我就得依次打开文件夹1、文件夹2、文件夹3、文件夹4,才能找到文件夹5。
(3)文件夹合并
对于文件夹的合并,也非常好理解,比如我现在桌面上有Boss和1两个文件夹。
很明显,Boss有老大的意思,因而即使1的手下人多,Boss也不在怕的,因而1乖乖地臣服于了Boss,变成了Boss的小弟…
于是我直接把文件夹1丢进了Boss文件夹里
因而Boss文件夹最终便变成了以下的结构:
整体结构示意图为:
Boss → boss
↓
1→2→3→4→5
3、并查集算法源码
(1)初始化
假设现在有n个文件夹,我们用一个数组f表示下标为i的文件夹的根文件夹,初始时每个文件夹的根文件夹为其自身
void initializeFolder(){
for(int i = 1; i <= n; i++)
f[i] = i;//每个文件夹的根文件夹为其自身
}
(2)查询
若我们需要查询文件夹x的根文件夹时:
int find(int x){
if(f[x] == x) return x;//如果x的根文件夹就是其自身
return find(f[x]); //否者递归查询找到x的根文件夹
}
(3)合并
当我们想把文件夹x所在的整个文件夹和y所在的整个文件夹合并时,我们需要需要先找到x的根文件夹a,再找到y的根文件夹b,再令a的根文件夹为b或令b的根文件夹为a即可。
void join(int x,int y){
int a = find(x);//找到x的根文件夹a
int b = find(y);//找到y的根文件夹b
if(a != b)//如果它们两不是同一个文件夹
f[a] = b;//令a的根文件夹为b
}
(4)路径压缩
第(2)步查询的时候,存在一个问题:我们每次找一个文件夹x的根文件夹时,都不会把这次查询的情况记录下来,因此,当下次我们再来查一次的时候,就得进行重复的繁琐操作,如下图所示:
我们第一次为了找到文件夹5的根文件夹Boss,我们就得依次查找上级文件夹4->3->2->1->Boss,但为了避免下一次查找时也进行如此繁琐的操作,我们便可以在第一次找完后直接令5的根文件夹为Boss
经过这番操作后,下次我们再来找文件夹5的根文件夹的时候,就只用查一次就够了。
int find(int x){
if(f[x] == x) return x;//如果x的根文件夹就是其自身
f[x] = find(f[x]);//递归查询找到x的根文件夹,并更新f[x]
return f[x];
}
(5)测试
#include<iostream>
using namespace std;
const int N = 5;
int f[N+1];
void initializeFolder(){
for(int i = 1; i <= N; i++)
f[i] = i;
}
int find(int x){
if(f[x] == x) return x;//如果x的根文件夹就是其自身
f[x] = find(f[x]);//递归查询找到x的根文件夹
return f[x];
}
void join(int x,int y){
int a = find(x);//找到x的根文件夹a
int b = find(y);//找到y的根文件夹b
if(a != b)//如果它们两不是同一个文件夹
f[a] = b;//令a的根文件夹为b
}
int main(){
initializeFolder();//文件夹初始化
join(1,2);//令文件夹1的根文件夹为2
join(1,3);//令文件夹1的根文件夹为3,即让文件夹2的根文件夹为3
join(2,3);//令文件夹2的根文件夹为3
join(3,4);//令文件夹3的根文件夹为4
join(4,5);//令文件夹4的根文件夹为5
//此时文件夹的目录级为5->4->3->2->1
cout << find(1) << endl;
cout << find(2) << endl;
cout << find(3) << endl;
cout << find(4) << endl;
cout << find(5) << endl;
}
运行结果:
来源:CSDN
作者:Ethan Hunt丶
链接:https://blog.csdn.net/weixin_43715601/article/details/104078103