1.基本概念
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
那并查集的作用是什么呢?
反复查找一个元素在哪个集合中,这一类问题反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。这就是我们为什么要学习并查集,而且一些要用DFS或BFS做的难题,用并查集也能够做出来。
2.基本思想
并查集的基本思想是将一些不相交集合的合并及查询问题通过树型数据结构来解决。并查集用于处理将元素组织成不相交集合的问题,并支持合并这些集合以及查询某些元素是否属于同一集合。其核心在于使用一个数组来表示整片森林(即多个不相交的树),其中每个树的根节点唯一标识了一个集合。通过这种方式,只要找到了某个元素的树根,就能确定该元素属于哪个集合。(在集合问题中非常常见)
并查集的实现通常包括两个主要操作:
-
合并操作:当需要将两个集合合并时,可以将一个集合的根节点连接到另一个集合的根节点下,从而将两个集合合并成一个。这种合并操作可以通过调整树的结构来实现,而不需要对所有元素进行复杂的修改。
-
查找操作:为了判断两个元素是否属于同一集合,需要判断它们的根节点是否相同。在并查集中,可以通过一种称为“路径压缩”的技术来优化查找操作,即在查找过程中直接将节点的父节点直接连接到根节点,从而减少后续查找的次数。
说这些可能太难理解了所以下面我找了一幅图,搭配着给上面的话解释一下:
图中的p和root就是两个集合的根节点
合并操作就如图红色箭头所示,将两个集合的根节点合并起来。
查找操作就如图中蓝色箭头所示,不断往上查找它的父节点,直到找到最后一个节点没有父节点的时候,那个节点就是根节点(找到根节点,可以把根节点保存下来,这样就避免了多次查找导致时间过长)
通俗点来讲 这就跟我们的家谱一样,我们要找我们的祖先,就要先找我们的父亲,我们的父亲再去找他的父亲......直到找到我们的祖先为止(相当于查找操作)
我们的父亲和母亲因为结婚所以他们两家人的祖先因此就变成了一家人,所以要不断往上找直到找到两家人各自的祖先,再把两家人各自的祖先合并成一个祖先(相当于合并操作)
如图所示:每一个人不断往上查找直到找到祖先(这幅图中相当于就是爷爷和奶奶)
为了保持并查集的性能,还可以进行平衡性优化。例如,在合并两个集合时,可以将较小的树的根节点连接到较大的树的根节点下,以避免树的高度过度增长,从而保持查找操作的时间复杂度在对数级别。这种优化有助于防止并查集退化为链表结构,确保了高效的查询和合并操作,这也是恩师给我们强调过的(恩师的金句里强调过)
把如何实现并查集的两个操作给讲了,那如何把它实现到代码上呢?
3.模板展示
有以更好理解,所以我这里就拿一道模板题过来讲解
这题连题目上都说了,【模板】并查集,所以这题很明显是一道并查集的模板题,用来学习并查集的。
通过读题我们知道他是想让我们把指定的数给合并起来,并且完成查询。
请看代码:
#include "bits/stdc++.h"
using namespace std;
const int N = 1e4+7;
int n, m, z, x, y;
int p[N];
int find(int x) {//用来执行查询操作的函数
if(x == p[x]) return p[x];//如果本身就是根节点,那么就直接return;
return p[x]=find(p[x]);//如果它的父节点不是跟结点的话,就继续往上找
}
void merge(int x, int y) {//用来执行合并操作的函数
int fx = find(x);
int fy = find(y);//套用查询操作的函数,来查找x,y的根节点
if(fx!=fy) p[fy] = fx;
return;
}
int main() {
cin >> n >> m;//输入元素和操作数
int tmp = m;
for(int i=1; i<=n; i++) p[i] = i;//令它们原本的根节点就是他们本身
while(tmp--) {
cin >> z >> x >>y;
if(z == 1) merge(x, y);//判断z如果为一就执行合并操作
else if(z==2){//判断z如果为二就执行查找操作
if(find(x) == find(y))cout << "Y\n";//根节点为一个就说明在一个集合中输出“Y”否则就不在输出“N”
else cout << "N\n";
}
}
return 0;
}
看完代码我们不难发现,并查集它也运用了递归,在找祖先的时候,如果比你大一辈的不是你的祖先,那就再往上加一辈直到找到你的祖先为止。
4、做题巩固
看见这题有根能延伸,还能出现和根,那么首先要想到并查集,将人家给的所有合根的植物合并以后,遍历整个数组。如果是别的植物来找自己合根的话,那么自己的祖先没变还是自己,如果自己本身就没合根的话,自己的祖先还是自己,那么我们只需要遍历整个数组,看有几个植物的祖先还是自己,就证明有几个植物合根了。上代码:
#include "bits/stdc++.h"
using namespace std;
const int N = 1e6+7;
int n, m, x, y,k;
int p[N];
int ans;
int v[N];
int find(int x) {
if(x == p[x]) return p[x];
return p[x]=find(p[x]);
}
void merge(int x, int y) {
int fx = find(x);
int fy = find(y);
if(fx!=fy) p[fy] = fx;
return;
}
int main() {
cin >> n >> m;
for(int i=1; i<=n; i++){
for(int j=1;j<=m;j++){
p[(i-1)*m+j]=(i-1)*m+j;
}
}
cin>>k;
while(k--) {
cin >> x >>y;
merge(x, y);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if((i-1)*m+j==p[(i-1)*m+j]){
ans++;
}
}
}
cout<<ans;
return 0;
}
5.一探究竟
树形结构中有一个特例就是几个节点组成了一个环,如何判断它组成了一个环呢?这很简单我们只需要用并查集来判断,就轻而易举地解决了,只要两个节点的祖先是同一个,我们就可以判断它们几个节点组成了一个环。光说很难理解我们就做一道题来演示一下。
奥赛一本通中的格子游戏1347:【例4-8】格子游戏
刚开始的时候,每个点都是各自的祖先,在两点之间画上边之后,就把两个点合并到一个集合中了,如果到了某一步,两个点已经在集合中了,就说明形成了一个闭环,在这步结束了游戏,直接输出就可以了,理论可行,实践出真知:
#include<bits/stdc++.h>
using namespace std;
int n, m, num = 1, x, y, t = 1;
char z;
int gz[207][207];
int a[40007];
bool cmp;
int find(int k) {
if (k == a[k]) return k;
else return a[k] = find(a[k]);
}
void ma1() {
int fx = find(gz[x + 1][y]);
int fy = find(gz[x][y]);
if (fx == fy) {
cmp = true;
return;
} else {
a[fx] = a[fy];
}
return;
}
void ma2() {
int fx = find(gz[x][y + 1]);
int fy = find(gz[x][y]);
if (fx == fy) {
cmp= true;
return;
} else {
a[fx] = a[fy];
}
return;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n*n; i++) {
a[i] = i;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
gz[i][j] = num;
num++;
}
}
for (int i = 1; i <= m; i++) {
cin >> x >> y >> z;
if (z == 'D') {
ma1();
if (cmp) {
cout << t;
return 0;
}
} else {
ma2();
if (cmp) {
cout << t;
return 0;
}
}
t++;
}
cout << "draw";
return 0;
}
因为它分向下和向左连接,所以我们写两个合并函数,每次连完之后判断它是不是一个闭环,如果是那么就直接输出当前次数,如果不是就接着连。将全部给的都连完之后如果还没有闭环说明比赛就没结束,那么直接输出“draw”。
关于并查集的作者全部都讲完了,感谢大家的观看。
你都看到这了,给作者点一个赞和关注吧,谢谢。
求个赞,阿里嘎多。你一票,我一票,我明天就能出道。