一.并查集的基本特性
- 并查集从集合上讲,主要是解决一些元素分组的问题,管理一系列不相交的集合
主要有两个操作:
Union:将两个不相交的集合合并为一个集合
Find:查询两个元素是否再同一个集合中
- 并查集从图角度讲,主要是解决动态连通性问题,可以以家族(朋友圈)为例子,你爸爸的亲戚也是你的亲戚。
连通的特性 1.自反性:p和p连通 2.对称性:若p和q连通,则q和p连通 3.传递性:若p和q连通,q和r连通,则p和r连通
二.并查集的基本思路:
1.模型理解:可以想象将是一个森林,每一棵树代表一个连通图,不同的树代表不同的连通图,它们之间没有交集
- 如果两个节点被连通,他们一定有相同的根节点
- 在合并两个树时,可以将一个节点的根节点连通到另一节点的根节点上(可以根据rank平衡)
2.基本结构:
class UnionFind{
int count; //记录连通分量的个数
vector<int> parent;//记录若干树的森林
vector<int> size; //记录连通分量的大小
int find(int a);
bool isConnect(int a,int b);
void Union_xx(int a,int b);
int getCount();
}
- parent数组中,记录的是每个节点的父节点,相当于指向父节点的指针,根节点指向自己
- size 数组,记录的是每棵树的大小,方便以后合并树时保持平衡,否则会退化成链表
- find函数,路径压缩,保持树的高度为常数
3.平衡性优化:
正情况下,查找一个树的节点的时间复杂度为log(n),但是如果这棵树极度不平衡,就会退变成链表,时间复杂度为O(n),为防止这种情况的发生,我们在合并一棵树的时候,可以将小一点的树根节点大的树的根节点的下面
void Union(int x,int y){
int rootp=find(x);
int rootq=find(y);
if(rootp==rootq)
return ;
if(size[rootp]<size[rootq]){
parent[rootp]=rootq;
size[rootq]+=size[rootp];
}
else{
parent[rootq]=rootp;
size[rootp]+=size[rootq];
}
count--;
}
4.路径压缩: 通过一次次find的调用,把每棵树的高度保持在为常数
//法1
int find(int x){
if(x!=parent[x])
parent[x]=find(parent[x]);
return parent[x];
}
//法2
int find2(int x){
while(x!=parent[x]){
parent[x]=parent[parent[x]];
x=parent[x];
}
return parent[x];
}
- 法1,调用完成后,路径上的父节点都是根节点
- 法2,需要经过多次调用才能达到上述效果 ,饱和前每次调用都会压缩一些路径。
三.并查集的实现
class UF{
private:
vector<int> parent; //通过指向父节点,存储多棵树组成的森林
vector<int> size; //记录一棵树的重量,即根节点的大小
int count=0; //连通分量个数
public:
int find(int x){
if(x!=parent[x]){
parent[x]=find(parent[x]); //路径压缩
}
return parent[x];
}
int find2(int x){
while(x!=parent[x]){ //路径压缩
parent[x]=parent[parent[x]];
x=parent[x];
}
return parent[x];
}
UF(int n){
parent.resize(n);
size.resize(n);
count=n;
for(int i=0;i<n;i++){
parent[i]=i;
size[i]=1;
}
}
int getCount(){
return count;
}
bool isConnect(int a,int b){
int root_a=find(a);
int root_b=find(b);
return root_a==root_b;
}
void Unoin_xxx(int a,int b){
int root_a=find(a);
int root_b=find(b);
if(root_a==root_b)
return;
//小树拼接到大树上,平衡
if(size[root_a]>size[root_b]){
parent[root_b]=root_a;
size[root_a]+=size[root_b];
}
else{
parent[root_a]=root_b;
size[root_b]+=size[root_a];
}
count--;
}
};
四.并查集的应用
并查集:
- 等式传递关系,
- 前后依赖关系,
- 逆向合并计算连通域的分裂情况(803. 打砖块 )
- 有向图是否有环
1.等式传递关系
- 399. 除法求值,已知a/b=v1,b/c=v2,求a/c
unordered_map<string,string> parent;
unordered_map<string,double> weight;
void initUF(vector<vector<string>> &equations){
for(auto &eq:equations){
if(parent.find(eq[0])==parent.end()){
parent[eq[0]]=eq[0];
weight[eq[0]]=1.0;
}
if(parent.find(eq[1])==parent.end()){
parent[eq[1]]=eq[1];
weight[eq[1]]=1.0;
}
}
}
string find_root(string &a){
if(a!=parent[a]){
string root=find_root(parent[a]);
weight[a]*=weight[parent[a]]; //若a->b->c,递归式更新权值,先更新w[b],再w[a];
parent[a]=root; //若a->b->c,其parent[x]=c;
}
return parent[a];
}
void union_Eq(string &a,string &b,double v){
string root_a=find_root(a);
string root_b=find_root(b);
if(root_a==root_b)
return ;
parent[root_a]=root_