并查集能做什么
并查集能高效回答一类类似连接的问题,具体的如迷宫中两点是否连通,图论中求最小生成树时判断是否形成环等。
基本操作
对于一组数据,主要支持两个操作:
- union(p,q):将p和q合并在一起;
- find(p):查p是属于哪个组的。
用来回答一个问题:
- isConnected(p,q):回答p和q是否连在一起。
Quick Find
最初级版本的并查集,能高效的进行find操作,只需要O(1)的时间复杂度。
class UnionFind {
private:
int *id; // 我们的第一版Union-Find本质就是一个数组
int count; // 数据个数
public:
// 构造函数
UnionFind(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++)
id[i] = i;
}
// 析构函数
~UnionFind() {
delete[] id;
}
// 查找过程, 查找元素p所对应的集合编号
int find(int p) {
assert(p >= 0 && p < count);
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
bool isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID)
return;
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++)
if (id[i] == pID)
id[i] = qID;
}
};
分析:Quick Find在find的时候只需要返回其id值就行了,故能高效的进行find操作,只需要O(1)的时间复杂度,因此而得名。但是,其在合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并,时间复杂度是O(n),效率比较低需要进一步优化。
Quick Union
实现思路:将每个元素,看做一个节点,把连在一起的节点看做一颗子树。
class UnionFind{
private:
// 我们的第二版Union-Find, 使用一个数组构建一棵指向父节点的树
// parent[i]表示第i个元素所指向的父节点
int* parent;
int count; // 数据个数
public:
// 构造函数
UnionFind(int count){
parent = new int[count];
this->count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for (int i = 0; i < count; i++)
parent[i] = i;
}
// 析构函数
~UnionFind(){
delete[] parent;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
int find(int p){
assert(p >= 0 && p < count);
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while (p != parent[p])
p = parent[p];
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
bool isConnected(int p, int q){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
parent[pRoot] = qRoot;
}
};
路径压缩
class UnionFind{
private:
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
int* rank;
int* parent; // parent[i]表示第i个元素所指向的父节点
int count; // 数据个数
public:
// 构造函数
UnionFind(int count){
parent = new int[count];
rank = new int[count];
this->count = count;
for (int i = 0; i < count; i++){
parent[i] = i;
rank[i] = 1;
}
}
// 析构函数
~UnionFind(){
delete[] parent;
delete[] rank;
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
int find(int p){
assert(p >= 0 && p < count);
// path compression 1
while (p != parent[p]){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
// path compression 2, 递归算法
// if( p != parent[p] )
// parent[p] = find( parent[p] );
// return parent[p];
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
bool isConnected(int p, int q){
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if (rank[pRoot] < rank[qRoot]){
parent[pRoot] = qRoot;
}
else if (rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
};