什么是并查集?
将N
个不同元素,按照某些相似特征划分到不同的M(M<=N)
个集合中
实现对不同元素的合并,查找某个元素属于哪个集合,描述这类问题的抽象数据类型就称之为并查集
并差集有哪些常见使用场景?
-
实现等价类的划分
-
构造最小生成树的Kruskal算法通过并查集判断最小生成树的连通性
-
不同集合中判断任意两个元素是否同属于一个集合
数据结构如何实现?
- 对于
N
个元素,构造一个能表示N个元素的索引数组parent[N]
,初始时初始化parent[i]
值为-1
,`表示只有一个元素的集合 - 把具有相似特征的元素的索引,合并为同一个集合,并用在数组
parent[i]
表示出来 - 若
parent[i]
值为负数,表示为根节点,且其绝对值为当前集合元素的个数;parent[i]
值为正数,表示的是指向根节点的索引 - 同一个集合中的各个元素,指向唯一的根索引;不同集合中元素,指向不同的根索引
实现过程
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <string>
using namespace std;
// 并查集
class UFSet {
private:
// parent[] 并不保存元素的值,只保存元素索引,并表示元素的关联关系
int *parent;
//集合中元素的个数
int size;
public:
UFSet(int sz);
~UFSet() { delete[] parent; }
bool Union(int rootpos, int childpos);
//查找值等于 pos 的根索引,如果索引为pos的元素是孤立的元素,则返回本身的索引;
//如果不是孤立的,跟其他元素共M个元素处在一个集合中,那么返回根元素的索引
int Find(int pos);
UFSet &operator=(UFSet &R);
void weightUnion(int rootpos, int childpos);
//折叠压缩规则压缩路径算法
int CollapsingFind(int pos);
void printUFSet();
};
// 初始化为单个元素
UFSet::UFSet(int sz) {
size = sz;
parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = -1;
}
}
UFSet &UFSet::operator=(UFSet &R) {
size = R.size;
parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = R.parent[i];
}
return *this;
}
// 查找 pos 的根
// Find性能:搜索时间不会超过树的高度加1
int UFSet::Find(int pos) {
if (pos < 0 || pos >= size) {
cout << "pos索引非法" << endl;
return -1;
}
while (parent[pos] >= 0) {
pos = parent[pos];
}
return pos;
}
// 把两个不相交的集合合并 root指示 pos,执行一次Union时间为O(1),N次为O(N)
// 性能:并查集的Union合并与Find查找实现虽然简单,但性能不好
// 当N个元素自成一个单元素的集合S[i],且树结构为N个棵树组成森林,有parent[k]=-1,其中i,k取值[0,n)
// 执行一次Find(i)要从被搜索元素出发,沿父指针链逐个走到根,时间为O(i),N次搜索需要时间为O(N^2),因此产生了退化的树
bool UFSet::Union(int rootpos, int childpos) {
//如果 childpos 已合并过,则不合并
if ((rootpos >= 0 && rootpos < size) && (childpos >= 0 && childpos < size) &&
(parent[childpos] == -1)) {
parent[rootpos] += parent[childpos];
parent[childpos] = rootpos;
return true;
}
return false;
}
// 加权规则改进 Union,可减少路径长度,把节点少的树合并到节点多的树作为子树
// 性能:执行一次weightUnion的时间比执行一次Union多,但仍然在O(1)范围内
// 归纳法证明,设置T为一系列合并操作weightUnion建立的M个节点的树,
// 则T的高度不会超过log2M(以2为底M的对数,向下取整)
void UFSet::weightUnion(int rootpos, int childpos) {
int r1 = Find(rootpos), r2 = Find(childpos);
int temp;
//根索引不等,说明不属于同一棵树(同一集合)
if (r1 != r2) {
temp = parent[r1] + parent[r2];
}
//因为parent[r2]、parent[r1]均小于0,所以当parent[r2] < parent[r1]时,节点r2数量大于节点r1的数量
//此时可将r1合并到节点r2下面,让r1指向r2
if (parent[r2] < parent[r1]) {
parent[r2] = temp;
parent[r1] = r2;
} else {
parent[r2] = r1;
parent[r1] = temp;
}
}
//折叠压缩规则,完成单个搜索,所需时间大约增加一倍,但这能减少在最坏情况下执行一些列操作所需时间
int UFSet::CollapsingFind(int pos) {
int j;
for (j = pos; parent[j] >= 0; j = parent[j])
;
while (pos != j) {
int temp = parent[pos];
parent[pos] = j;
pos = temp;
}
return j;
}
//输出并查集元素索引
void UFSet::printUFSet() {
for (int i = 0; i < size; i++) {
cout<<"根索引:"<<i<<", 包含元素索引:";
for(int j=0;j<size;j++){
if(i!=j && Find(j)==i){
cout<<j<<",";
}
}
cout<<endl;
}
}
int main() {
//这里有多个元素,每个元素都是独立的,这些元素不重复
int arr[] = {8, 6, 4, 2, 0, 1, 3, 5, 7, 9};
int n = sizeof(arr) / sizeof(int);
UFSet p(n);
//将arr中独立的元素合并,合并索引为0,1,2,3对应元素为{8,6,4,2}为一个集合
//合并索引5,6,7,8对应元素为{1,3,5,7}为一个集合
//其他索引4,9对应孤立的元素0,9不构成集合
p.Union(0, 1);
p.Union(0, 2);
p.Union(0, 3);
p.Union(5, 6);
p.Union(5, 7);
p.Union(5, 8);
cout << "查找索引为3的根元素索引:" << p.Find(3) << endl;
cout << "查找索引为8的根元素索引:" << p.Find(8) << endl;
cout << "查找索引为4的根索引(孤立元素本身):" << p.Find(0) << endl;
int pos = p.Find(7);
cout << "查找索引为7的根索引:" << pos << ",元素值为:" << arr[pos] << endl;
p.printUFSet();
return 0;
}