声明
本文章的内容来源于《abuladong的算法小抄》,为了加强自己的记忆,写本文章梳理学习内容。
算法简介
本算法为通常说的并查集算法,主要解决图论中“动态连通性”问题。数据结构(清华版)课本上有一节专门讲了这个算法。
问题
一个图(无向图)有5个节点(1~5),他们不是相互连通的,那么我们认为连通分量为5个。
在此我们可以实现Union-Find算法的API:
class UnionFind{
public:
// 将p节点和q节点连通
void union(int p, int q);
//判断p节点和q节点是否连通
bool connect(int p, int q);
//返回图中有多少个连通分量
int count();
}
这里得连通是建立在无向图之上的。
如果现在调用union(1,2),那么节点0和节点1被连通,连通分量就变成4个。
再调用union(2,3),那么这时1,2,3节点都被连通,再调用connected(1,3)也会返回true,连通分量变成8个。
由此可见,本算法的关键在于union和connected函数的效率。
基本思路
模型:使用森林来表示图的动态连通性
数据结构:数组实现这棵树
表示连通性:设定数的每个节点用一个指针指向其父节点,如果是根节点,那么其父节点就是他自己。
代码实现:
#include<iostream>
#include<vector>
using namespace std;
class UnionFind{
private:
//记录连通分量
int count;
//记录其父节点
vector<int> parent;
public:
//构造函数
UnionFind(int n){
//开始都不连通
this->count = n;
//初始化父节点都是指向自己
this->parent.resize(n);
for(int i = 0; i < n; i++) this->parent[i] = i;
}
void Union(int p, int q){
//找根节点
int rootp = find(p);
int rootq = find(q);
//如果根节点相同 说明两个节点是连通的
if(rootq == rootp) return;
//跟节点连接起来
// 下面两种连接都可以
// this->parent[rootq] = rootp;
this->parent[rootp] = rootq;
this->cout--;
}
//找根节点
int find(int x){
while(x != this->parent[x]) x = this->parent[x];
return x;
}
//返回连通分量
int count(){
return this->count;
}
bool Connected(int p, int q){
int rootq = find(q);
int rootp = find(p);
//根节点相同 说明是连通的
return rootp == rootq;
}
};
如果两个节点被连通,则让其中一个节点的根节点接到另一个节点上。
如果两个节点拥有相同的根节点,说明这两个节点是连通的。
时间复杂度:find,union,connected的最坏时间复杂度是O(N)
如果数据量巨大,这种复杂度是不可以接受的,主要的原因是我们构建的树模型存在不平衡现象
平衡优化
出现不平衡问题主要出现在union现象
我们只是简单粗暴的将一个树的根节点接到另一棵树的根节点下面,这样会产生极度不平衡现象。
将小一些的树接到大一些树的下面,这样就可以更加平衡一些。我们定一个变量,记录每一颗树的节点数目。
class UnionFind{
private:
//记录连通分量
int count;
//记录其父节点
vector<int> parent;
//增加数组,记录树包含的节点数
vector<int> size;
public:
//构造函数
UnionFind(int n){
//开始都不连通
this->count = n;
//初始化父节点都是指向自己
this->parent.resize(n);
this->size.resize(n);
for(int i = 0; i < n; i++) {
this->parent[i] = i;
this->size[i] = 1;
}
}
void Union(int p, int q){
//找根节点
int rootp = find(p);
int rootq = find(q);
//如果根节点相同 说明两个节点是连通的
if(rootq == rootp) return;
//跟节点连接起来
// 下面两种连接都可以
// this->parent[rootq] = rootp; //q树接到p树上 ***注意
// this->parent[rootp] = rootq;
//小树接到大树的下面
if(this->size[rootp] > this->size[rootq]){
//p树的节点多,为大树, q树接到p树上 ***注意
this->parent[rootq] = rootp;
this->size[rootp] += this->size[rootq]
}
else{
this->parent[rootp] = rootq;
this->size[rootq] += this->size[rootp]
}
this->cout--;
}
//其他函数不变
}
此时 find、union、connected的时间复杂度标成了O(Nlog(N))
注意 this->parent[rootq] = rootp; 是q树接到p树上,在写次博客的时候差点把自己弄晕了。
路径压缩
我们可以进行路径压缩,让树的高度保持常数,这样find就可以用O(1)的时间复杂度找到某个根节点,相应的connected和union函数也降到了O(1)
int find(int x){
// while(x != this->parent[x]) x = this->parent[x];
while(parent[x] != x){
// 只需要加入这一样即可
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
有了压缩路径,那么记录树节点个数的size数组还要么?
可以不要,但是加上会效率高一些,可以让树更平衡一些。所以,路径压缩和重量平衡都用上是最好的选择。
完成代码
#include<iostream>
#include<vector>
using namespace std;
class UnionFind{
private:
//记录连通分量
int count;
//记录其父节点
vector<int> parent;
//增加数组,记录树包含的节点数
vector<int> size;
public:
//构造函数
UnionFind(int n){
//开始都不连通
this->count = n;
//初始化父节点都是指向自己
this->parent.resize(n);
this->size.resize(n);
for(int i = 0; i < n; i++) {
this->parent[i] = i;
this->size[i] = 1;
}
}
void Union(int p, int q){
//找根节点
int rootp = find(p);
int rootq = find(q);
//如果根节点相同 说明两个节点是连通的
if(rootq == rootp) return;
//跟节点连接起来
// 下面两种连接都可以
// this->parent[rootq] = rootp; //q树接到p树上 ***注意
// this->parent[rootp] = rootq;
//小树接到大树的下面
if(this->size[rootp] > this->size[rootq]){
//p树的节点多,为大树, q树接到p树上 ***注意
this->parent[rootq] = rootp;
this->size[rootp] += this->size[rootq]
}
else{
this->parent[rootp] = rootq;
this->size[rootq] += this->size[rootp]
}
this->cout--;
}
//找根节点
int find(int x){
// while(x != this->parent[x]) x = this->parent[x];
while(parent[x] != x){
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
//返回连通分量
int count(){
return this->count;
}
bool Connected(int p, int q){
int rootq = find(q);
int rootp = find(p);
//根节点相同 说明是连通的
return rootp == rootq;
}
};
int main(){
UnionFind A = UnionFind(10);
return 0;
}
除了构造函数的时间复杂度是O(N),其他的时间复杂度都是O(1)
结束
如果有读者需要图来解释的话,等我忙完这段时间再补上。