并查集是一种非常精巧而且实用的数据结构,主要用于处理一些不相交集合的合并问题。
通常的解释可以理解为,在一个城里有n个帮派,例如假设a,b是两个人并且认识,a是A帮派的人,a和b认识则b也会是A帮派的人,c认识b,那么c也会是A帮的人,现在将给出所有的人的认识关系,判断这里有多少帮派以及每个人属于哪个帮派,那么这里的合并查询问题便可以用并查集来解决。
并查集:将编号1~n的n个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合,在这个集合中,并查集实现初始化,合并以及查找操作。
并查集的简单实现
初始化:定义数组s[x]是以x为元素的并查集,开始的时候还没有给出合并关系,因此每个元素都是一个独立的集合,并且以元素x表示它的集s[x]。
例:
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1000;
int s[maxn];
void init()
{
for (int i = 0; i < maxn; i++)
{
s[i] = i;
}
}
将数组中每个元素都赋值为独立的集。
合并: 读取到关系之后在并查集中将节点合并起来。
例如将1合并到2的具体操作为,将下标为1的数组元素的值改为下标为2的数组元素的值,因而可以使得以1为下标访问时其指向的集合和下标为2的数组相同。再将2和1合并到3的时候将下标为2的数组的值改为3的值即可。思路为递归思路,访问s[3]=s[s[2]]=s[s[s[1]]],以数组值作下标使用。
void uni(int x, int y)
{
x = find(x);
y = find(y);
if (x != y)
s[x] = s[y];
}
find函数的操作即为找到根节点。
查找根节点: 查找操作即为递归操作,使得元素的值和集相等便中止。
int find(int x)
{
return x==s[x]?x:find(s[x]);
}
在这种操作下,查找合并的操作时间复杂度均为O(n)性能较差,可以使其优化为O(log2n)。
优化合并
在进行简单的合并操作时候,一昧的同样合并可能会使得部分数据会变成一个链表结构,并且随着深度的加深,访问也会越来越慢,在这种情况下可以先判断两个集的深度,使得深度小的合并到深度大的上可以使得减少树的高度。
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1000;
int s[maxn];
int height[maxn];
void init()
{
for (int i = 0; i < maxn; i++)
{
s[i] = i;
height[i] = 0;//初始化
}
}
void uni(int x, int y)
{
x = find(x);
y = find(y);
if (height[x] >= height[y])//深度判断,深度一样的情况下可以任意
{
s[y] = x;
height[x]++;
}
else
{
s[x] = y;
height[y]++;
}
}
路径压缩
在之前的查找操作中,优化完合并操作之后部分树的高度虽然减少但是仍然存在,如果可以将每个元素都直接指向根节点的话访问的速度也会直接便为O(1),但是这之前是必须要全部访问一遍的,因此路径压缩的操作为:
int find(int x)
{
if (x != s[x])
s[x] = find(s[x]);//将元素直接指向根节点
return s[x];
}
整个搜索路径上的元素所属的集都被改为了根节点,因此可以使得下次合并或者查询时候的复杂度大大降低。
PS:并查集的思想的话在接触之前也是有点想过的,以数组的值作为数组下标使用,配合递归可以实现一些操作,不过以上这些也算是个人理解,可能会在个别地方有错,欢迎大佬改正