目录
并查集(Disjoint Set Union, DSU)是一种数据结构,它有些像图,但不是图。本文最后附上一般可以直接用的代码。
并查集(Disjoint Set Union, DSU)是一种用于处理一些不交集的合并及查询问题的数据结构。
1、基本概念
他是有向的。并查集的三个性质:互异性(能够合并两个节点)、无序性(并查集不管顺序)、确定性。
我们举一个例子:
一个并查集可以像这样:(随便画的)
每一个点就是一个元素或(也叫)节点。
这是它的数据:
1 => 2;3 => 4;2 => 4。
先把元素1和元素2合并,形成一个集合,取一个元素1为代表(在启发式合并(一种优化方法)中是取最大的数,待会讲)
再把元素3和元素4合并,取元素3为代表,最后问题来了,2和4已经不在并查集中(可以这么理解)(被合并了)。所以是最后把元素1和元素3合并,取元素1为代表,最后形成一个大集合。最后每个元素单独属于一个集合
并查集主要需要以下三个功能(建议定义函数):
初始化并查集、查找、合并,详见本文最后的代码。
2、优化并查集
虽然并查集已经足够实用,但仍然可以继续优化以降低时间复杂度等方面。
2.1 路径压缩(Union-Find)
路径压缩是并查集(Union-Find)数据结构中的一种优化方法。在使用并查集时,我们通常会维护一个代表元素数组,用于表示每个元素所属的集合。不适用路径压缩时,当我们想要查找某个元素所属的集合时,我们可能需要沿着父节点数组一直向上查找,直到找到该集合的代表元素。这个过程可能会涉及多个步骤,特别是当树形结构较深时。
路径压缩的目的是减少查找时间。当我们找到一个元素的代表元素时,我们可以将该元素直接连接到代表元素上,从而缩短查找路径。这样,在后续的查找操作中,我们就可以更快地找到代表元素。
具体来说,在路径压缩的实现中,当我们沿着父节点数组向上查找时,我们会将沿途的每个节点都直接连接到代表元素上。这样,原本可能需要多步才能到达代表元素的节点,现在只需要一步就可以到达。
路径压缩可以显著提高并查集的性能,通过减少查找路径的长度,它降低了查找操作的时间复杂度。
2.2 启发式合并(Heuristic Merge)
启发式合并(Heuristic Merge)在并查集(Union-Find)数据结构中,主要目的是优化合并操作,以减少后续查找操作的时间复杂度。其核心思想是在合并两个集合时,选择其中一个较大的集合,将另一个较小的集合合并到这个较大的集合中。
具体点说,启发式合并通过比较两个集合的大小,总是将较小的集合合并到较大的集合中。这样,随着时间的推移,合并操作会倾向于创建较大的集合,从而减少集合的总数。由于查找操作的时间复杂度与集合的深度有关,较大的集合通常具有较浅的深度,因此启发式合并有助于减少后续查找操作的时间。
启发式合并通常涉及维护一个额外的数组来存储每个集合的大小或秩。在合并两个集合时,比较它们的大小或秩,然后将较小的集合的根节点的父指针指向较大集合的根节点,并更新较大集合的大小或秩。
总的来说,启发式合并是一种优化策略,旨在通过减少集合的数量和深度来加速并查集的合并和查找操作。
3.3 按秩合并(Union by Size)
按秩合并的目的是为了减少Find操作的复杂度,即减少查找某个元素所在集合代表(或称为根节点)时的路径长度。在按秩合并中,“秩”通常定义为集合的大小(即集合中元素的数量)或集合中树的高度。合并时,我们将秩较小的集合合并到秩较大的集合中,这样秩较大的集合的根节点保持不变,而秩较小的集合的所有元素都直接连接到秩较大的集合的根节点上。
通过这种方式,我们可以保证每个元素到其所在集合的根节点的路径长度不会超过O(log n),其中n是并查集中元素的总数。这是因为每次合并都会使得至少一个集合的大小翻倍,因此合并操作的次数不会超过O(log n)。由于Find操作涉及到沿着路径向上查找根节点,因此Find操作的复杂度也是O(log n)。
总的来说,按秩合并是一种优化策略,旨在通过减少Find操作的复杂度来提高并查集的性能。
3、直接用的代码模板
用于测试的功能是指main函数中用于测试功能的代码。
3.1 没有按秩合并优化和用于测试的功能的代码
以下是代码(包含了初始化、查找、合并操作):
#include <iostream>
#define MAXN 100000
// 并查集最大大小
int f[MAXN], fa[MAXN], size[MAXN], n, m, x, y ;
void init()
{
// 初始化函数,一定不要忘记
for (int i = 1; i <= n; i++)
{
f[i] = i;
}
}
int find(int x)
{
// 路径压缩优化后的查找函数
if (x == fa[x])
{
return x;
}
fa[x] = find(fa[x]);
return fa[x];
}
void join(int c1, int c2)
{
// 启发式合并优化后的合并函数
int f1 = find(c1), f2 = find(c2);
if (f1 != f2)
{
if (size[f1] < size[f2])
{
std::swap(f1, f2);
}
fa[f2] = f1;
size[f1] += size[f2];
}
}
int Not_optimized_find(int x)
{
// 未优化的查找函数
if (x == fa[x])
{
return x;
}
return find(fa[x]);
}
void Not_optimized_join(int f, int s)
{
// 未优化的合并函数
int fd = find(f);
int sd = find(s);
if (fd != sd)
{
fa[fd] = sd;
}
}
int main()
{
return 0;
}
3.2 有按秩合并优化和用于测试的功能的代码
以下是代码(包含了初始化、查找、合并、按秩合并、用于测试的功能,一般可以直接套了用):
#include <iostream>
#define MAXN 100000
// 并查集最大大小
int f[MAXN], fa[MAXN], size[MAXN], n, m, x, y;
void init()
{
// 初始化函数,一定不要忘记
for (int i = 1; i <= n; i++)
{
f[i] = i;
fa[i] = i; // 初始化fa数组
size[i] = 1; // 初始化每个集合的大小为1
}
}
int find(int x)
{
// 路径压缩优化后的查找函数
if (x == fa[x])
{
return x;
}
fa[x] = find(fa[x]);
return fa[x];
}
void join(int c1, int c2)
{
// 启发式合并优化后的合并函数
int f1 = find(c1), f2 = find(c2);
if (f1 != f2)
{
if (size[f1] < size[f2])
{
std::swap(f1, f2);
}
fa[f2] = f1;
size[f1] += size[f2];
}
}
int main()
{
std::cout << "Enter the number of elements: ";
std::cin >> n;
init();
std::cout << "Enter the number of operations: ";
std::cin >> m;
for (int i = 0; i < m; i++)
{
std::cout << "Enter two elements to join: ";
std::cin >> x >> y;
join(x, y);
}
int totalSets = 0;
for (int i = 1; i <= n; i++)
{
if (fa[i] == i)
{
totalSets++;
std::cout << "Set " << i << " size: " << size[i] << std::endl;
}
}
std::cout << "Total number of sets: " << totalSets << std::endl;
return 0;
}