2021.11.28 并查集
并查集
一、关于并查集
1.1 并查集概念引入
并查集,顾名思义,就是合并查询集合,是一种特殊的树状数据结构。一般是用来解决元素分组的问题,或是管理一些不相交的集合,他支持两种操作:1.合并(Union)2.查询(Find),比较典型的问题就是找亲戚,最小生成树的Kruskal算法等等,我们可以考虑使用并查集进行维护。。
并查集的主要思想是用一个集合中的元素来代表这个集合。
这么说可能有点抽象,有人用武侠的例子来生动刻画了这个并查集,有兴趣可以了解一下 链接在这.
简单来说,就是有一堆不认识的人,他们需要拉帮结派,这里需要解决两个问题,第一,怎么让他们结盟(合并),第二,怎么知道他们是不是同一个帮派(查询)
1.2 合并
根据这几张图可以感受一下他们是怎么样建立关系的
初始状态下大家都是分散的,各自成一派,所以自己的教主都是自己
后面2和3想加入1的帮派,于是将他们的教主设为1,这样我就只要知道他们教主是谁,就知道他们是不是这帮派的人
5 6和4同盟
4又和1同盟了,那么大家全都是一家人,英文我们的教主都是1,咱们的头是一样的,那肯定是一帮子人。
最后的树结构是这样的
另外提一下,这里的教主比较专业的叫法是生成元。我们并查集就是通过生成元来判断是不是隶属于这个集合的
1.3 查询
我们已经知道了他们怎么在同一个集合里的,不就是看他们的教主嘛,那我查他们是不是一路子人,就看他们教主是不是一样的就可以了。
那现在的主要问题就是怎么知道他的教主。
刚刚最后的图样很明显,是一个树形的数据结构,我们通过访问其上一级(引入pre数组访问),访问到最上面的一层以后肯定能找到教主的。所以我们很自然想到的方法就是递归。
直接上代码。
二、代码实现
初始化(每个人就是一个帮派,自己的教主就是自己本身)
void init(int num)
{
//一开始是盘散沙,每个人都是各立一派,
//所以自立为王,他们的上一级(pre)就是他们自己
for (int i = 1; i <= num; i++)
{
pre[i] = i;
}
}
找生成元(递归思想)
int find(int key)
{
if (pre[key] == key)
return key;
return find(pre[key]);
}
合并
void unite(int x, int y)
//并查集中的‘并’函数
//就是让x,y结成同盟(让x,y的老大都是一样的)
//即隶属于同一个集合内
{
int root_x = find(x);
int root_y = find(y);
if (root_x == root_y)
return;
else{
pre[root_x] = root_y;
}
}
三、一些优化
3.1 查询路径压缩
我们希望我们只要找到最后的教主就好了,但是每一轮我都这样递归访问,明显会有点麻烦,具体如图。
那有没有解决问题的办法呢?答案是有的,要解决递归引起的问题,我们就在递归上下点功夫,我们是否能将最后递归找到的教主,赋值给每一个之前递归过程中的元素呢?那我们多一个赋值操作就行了,后续我访问的时候,至少这些访问过的节点可以一步到位直接访问到教主,这就是那些大佬们总是说的路径压缩。
int find(int key)
//并查集中的’查‘函数
//找教主,要是不是自己的话,就递归往上找
//这边是优化了一下算法,在递归过程中又赋值一下
//只要查过了一次老大后面就只要调用一次就能找到老大(root)了
{
if (pre[key] == key)
return key;
return pre[key] = find(pre[key]);
}
厉害一点的可以直接一行三元运算符?: 的代码搞定
return x == fa[x] ? x : (fa[x] = find(fa[x]));
3.2 合并路径压缩(按秩合并)
那样优化以后,大家普遍会有一个认识误区,并查集最终生成的树深度是2,但是要注意路径压缩只在查询时进行,也只压缩一条路径,所以并查集最终的结构仍然可能是比较复杂的
在合并问题上,我们也可以留意一下,之前合并的操作的时候,或许你会想着一个问题,为什么要委屈x,让他的帮主上一级变成y的帮主,让y的帮主的上一级变成x的帮主不行吗?其实是可以的,合理的变,在路径上甚至还会有所优化。这是大佬们说的并查集按秩合并。
观察这个图可以发现,树的深度变小了,这意味着访问教主所要递归的次数变少了。具体我们应该把深度小的树合并到深度大的树上去,这样合并以后,到根节点距离变长的节点个数会更少。
代码上需要做的改变是
这里我们引入一个Rank数组,记录每一个节点对应的深度
初始化的时候我们做一点改变,刚开始的深度是1
void init(int n)
{
for (int i = 1; i <= n; ++i)
{
fa[i] = i;
rank[i] = 1;
}
}
合并操作代码优化
void unite(int x, int y)
{
int root_x = find(x);
int root_y = find(y);
//已经是同一个集合的话就不需要做了
if (root_x == root_y)
return;
//小树并大树
if (depth[root_x] > depth[root_y])
pre[root_y] = root_x;
else
{
if (depth[root_x] == depth[root_y])
depth[y]++;
pre[root_x] = root_y;
}
}
这里只有一种情况是要改变树的深度的,那就是当两棵树的深度相同时,深度要加1。小数并大树的时候,深度还是大树的深度。不会变的
请看,这是两个树深度相同的情况
插入到另外一组树后,深度会+1,因为插入的树上面顶着被插树的根节点,所以深度多了一,倒过来插也是一样的。
四、题目试炼
2017年第八届 蓝桥杯C组 C/C++决赛 合根植物
题目描述
w星球的一个种植园,被分成 m * n 个小格子(东西方向m行,南北方向n列)。每个格子里种了一株合根植物。
这种植物有个特点,它的根可能会沿着南北或东西方向伸展,从而与另一个格子的植物合成为一体。
如果我们告诉你哪些小格子间出现了连根现象,你能说出这个园中一共有多少株合根植物吗?
输入格式
第一行,两个整数m,n,用空格分开,表示格子的行数、列数(1<m,n<1000)。
接下来一行,一个整数k,表示下面还有k行数据(0<k<100000)
接下来k行,第行两个整数a,b,表示编号为a的小格子和编号为b的小格子合根了。
格子的编号一行一行,从上到下,从左到右编号。
比如:5 * 4 的小格子,编号:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20
样例输入
5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17
样例输出
5
其合根情况参考下图:
我的AC代码附上~
#include <stdio.h>
const int MAX_N = 1000005;
int pre[MAX_N], depth[MAX_N];
int m, n, x, y, T,sum;
void init(int num)
{
//一开始是盘散沙,
//每个人都是各立一派,所以自立为王,
//他们的上一级(pre)就是他们自己
for (int i = 1; i <= num; i++)
{
pre[i] = i;
depth[i] = 0;
}
}
int find(int key)
//并查集中的’查‘函数
//找教主,要是不是自己的话,就递归往上找,
//这边是优化了一下算法,在递归过程中又赋值一下,
//只要查过了一次老大后面就只要调用一次就能找到老大(root)了
{
if (pre[key] == key)
return key;
return pre[key] = find(pre[key]);
}
void unite(int x, int y)
//并查集中的‘并’函数
//就是让x,y结成同盟(让x,y的老大都是一样的)
//即隶属于同一个集合内
{
int root_x = find(x);
int root_y = find(y);
if (root_x == root_y)
return;
if (depth[root_x] > depth[root_y])
pre[root_y] = root_x;
else
{
if (depth[root_x] == depth[root_y])
depth[y]++;
pre[root_x] = root_y;
}
}
int main()
{
// freopen("1.in", "r", stdin);
// freopen("2.out", "w", stdout);
scanf("%d %d", &m, &n);
scanf("%d", &T);
init(m * n);
while(T--){
scanf("%d %d", &x, &y);
unite(x, y);
}
for (int i = 1; i <= m * n; i++)
{
if (find(i) == i)
{
sum++;
}
}
printf("%d\n", sum);
return 0;
}
后续关注更新~~~~