写在前面
并查集(Disjoint Sets)
在一些有N个元素的集合应用问题中,通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,并可以反复查找一个元素在哪个集合中
主要操作
- 初始化 Init:将每个元素所在集合初始化为自身
- 查找 Find:找某个元素所在集合
- 合并 Union:将两个元素集合合并为一个集合
我们可以用数组来表示所有集合,数组下标表示某个下标所对应的某个元素,下标所对应的的值表示包含这个元素的集合
为便于理解起见,将初始化后的集合可以理解为森林,集合中的每个节点都是树根节点,将一些树根节点合并之后,森林中的树根节点变少了,但是某些树变“壮实”了,也就是这些树根节点衍生出其子节点,这些子节点还有可能衍生出子节点…合并之后,规定:每个节点对应的下标的值就是它的父节点的下标,而根节点的下标就是根节点本身
如下图所示:
C++实现
class Disjoint_sets
{
private:
std::vector<int> DisjSets; // 森林集合
public:
// 初始化
int Disjoint_sets(int max_size)
: DisjSets(std::vector<int>(max_size)) // 为集合扩容
{
for(int i = 0; i < DisjSets.size(); i++)
{
DisjSets[i] = i; // 将集合中的每个元素下标作为它的值
}
}
// 找下标为index的节点的根节点(根节点的父节点是自身)
int Find(int index)
{
if(index != DisjSets[index])
{
// 当前下标为index的节点不是根节点,取当前节点中保存的父节点值递归查找
index = Find(DisjSets[index]);
}
return DisjSets[index]; // 返回根节点
}
// 合并两个节点----需要将它们的根节点进行合并
void Union(int index1, int index2)
{
index1 = find(index1); // 找index1节点的根节点
index2 = find(index2); // 找index1节点的根节点
if(index1 != index2)
{ // 如果不相等,就进行合并;如果相等,说明它们本来就是连通的,不需要合并
DisjSets[index1] = index2;
}
}
};
这种最原始的写法,最贴合并查集的定义,但是细想,里面 Disjoint_sets::Find() 是对这一操作最频繁,并且最为耗时,它的时间复杂度为O(n),n为树的高度,即当这个森林集合退回一棵单支树的情况下,若频繁使用 Disjoint_sets::Find() 操作,效率较低
优化
按秩合并
在和并两棵树时,将高度较小的树合并到高度较大的树上,这里使用“秩(rank)”来替代高度,秩表示高度的上界,当只有 一个节点的树的秩为0
将两棵秩分别为r1,r2的树合并,如果秩不相等,将秩小的树合并到秩大的树上,这样就能保证新树秩不大于原来的任意一棵树;如果秩相等,任意合并即可,新树的秩为 r1 + 1
路径压缩
在 Find 过程中,将路径上的所有节点直接连接到根节点上,这样的话,找根节点时效率就会大大提高
优化后代码
在上面的代码上进行优化,如下所示:
class Disjoint_setsII
{
private:
std::vector<int> DisjSets;
std::vector<int> rank; // 秩
public:
int Disjoint_setsII(int max_size)
: DisjSets(std::vector<int>(max_size))
: rank(std::vector<int>(max_size, 0)) // 初始化秩
{
for(int i = 0; i < DisjSets.size(); i++)
{
DisjSets[i] = i;
}
}
int Find(int index)
{
if(index != DisjSets[index])
{
// 当前下标为index的节点不是根节点,将当前节点挂到父节点上,并递归查找
DisjSets[index]= Find(DisjSets[index]); // 路径压缩
}
return DisjSets[index]; // 返回根节点
}
void Union(int index1, int index2)
{
index1 = find(index1); // 找index1节点的根节点
index2 = find(index2); // 找index1节点的根节点
if(rank[index1] > rank[index2]) // 按秩查找
{
DisjSets[index2] = index1; // 将 index2 合并到 index1 上去
}
else
{
DisjSets[index1] = index2; // 将 index1 合并到 index2 上去
if(rank[index1] == rank[index2])
{ // 秩相等时,将合并后的新树的秩++
rank[index2]++;
}
}
}
};
例题
原题出自蓝桥杯官网题库----合根植物
- 问题描述
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 - 样例说明
解答如下:
#include <iostream>
#include <vector>
using namespace std;
int m, n;
vector<int> uset;
void Init()
{
for(int i = 0; i <= n * m; i++)
{
uset[i] = i;
}
}
int Find(int n)
{
if(uset[n] != n)
{
uset[n] = Find(uset[n]);
}
return uset[n];
}
void Merge(int x, int y)
{
x = Find(x);
y = Find(y);
if(x != y)
{
uset[x] = y;
}
}
int main()
{
cin >> m >> n;
uset.resize(m * n + 1);
int k;
cin >> k;
Init();
for(int i = 0; i < k; i++)
{
int x, y;
cin >> x >> y;
Merge(x, y);
}
int result = 0;
for(int i = 1; i <= n * m; i++)
{
if(uset[i] == i)
{
result++;
}
}
cout << result << endl;
return 0;
}