并查集
# 并查集
将编号分别为 的 个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在的集合
# 实现
# 初始化
定义 表示元素 所属的并查集,我们可以简称为祖先,当初始时,元素的祖先就是他本身
struct dsu{
int fa[maxn];
dsu(){for(int i=1;i<=maxn;i++) fa[i]=i;}
}
2
3
4
struct dsu{
int n;
vector<int> fa;
dsu(int n){
this->n=n;
fa.resize(n+1);
for(int i=1;i<=n;i++) fa[i]=i;
}
};
2
3
4
5
6
7
8
9
# 合并
如果想把 合并,就把 的祖先改为 的祖先
然后把 合并,就先找到 的祖先 ,然后找到 的祖先,把 的祖先和 的祖先合并,在这里也就是 的祖先变成了
void merge(int x,int y){
x=find(x),y=find(y);
if(x!=y) fa[x]=y;
}
2
3
4
# 查找
查找就是查找祖先的过程,我们先找自己的祖先,如果自己的祖先不是自己,那么就一直查找下去。可以看到这颗查找树的高度可能很大,复杂度为 ,变成了一个链表
int find(int x){
return fa[x]==x?x:find(fa[x]);
}
2
3
# 合并的优化
注意到查找操作,合并操作的复杂度在极端情况下都为
如果考虑每次合并的时候,记录元素 在查找树中的高度 ,每次合并的时候,把高度较小的集合合并到较大的集合上,减小树的高度
struct dsu{
int n;
vector<int> fa,h;
dsu(int n){
this->n=n;
fa.resize(n+1);h.resize(n+1);
for(int i=1;i<=n;i++) fa[i]=i,h[i]=0;
}
int find(int x){
return fa[x]==x?x:find(fa[x]);
}
void merge(int x,int y){
x=find(x),y=find(y);
if(x==y) return ;
if(h[x]==h[y]){ //高度相同,随便合并
h[x]++;
fa[y]=x;
}
else{ //把矮的合并到高的上
if(h[x]<h[y]) fa[x]=y;
else fa[y]=x;
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
事实上,一般不需要合并的优化,因为在做了路径压缩之后,附带优化了合并
# 查询的优化--路径压缩
在查询的过程中,搜索的路径可能很长,但是每次查询的结果都是相同的,我们可以在返回时顺便把 所属的祖先改为根节点,下次再搜索的时候,就是 时间得到结果
int find(){
return fa[x]==x?x:fa[x]=getfa(fa[x]);
}
2
3
# 启发式合并
路径压缩最坏时间复杂度是
将节点较少或深度较小的树连到另一棵,我们把节点数或深度作为唯一指标合并
void merge(int x,int y){
int fx=find(x),fy=find(y);
if(fx!=fy){
fa[fx]=fy;
if(size[fx]>size[fy]) swap(fx,fy);
fa[x]=fy;
size[fy]+=size[fx];
}
}
2
3
4
5
6
7
8
9
# 删除
要删除一个叶子节点,我们可以将其父亲设为自己。为了保证要删除的元素都是叶子,我们可以预先为每个节点制作副本,并将其副本作为父亲。
struct dsu{
int fa[maxn<<1],size[mxan<<1];
dsu(){
for(int i=1;i<=maxn;i++) fa[i]=i+maxn,size[i]=1;
for(int i=maxn+1,i<=mxan;i++) fa[i]=i,size[1]=1;
}
void erase(size_t x) {
--size[find(x)];
pa[x] = x;
}
}
2
3
4
5
6
7
8
9
10
11
# 移动
与删除类似,通过以副本作为父亲,保证要移动的元素都是叶子
void move(int x,int y){
int fx=find(x),fy=find(y);
if(fx==fy)return ;
fa[fx]=fy;
--size[fx],++size[fy];
}
2
3
4
5
6
# 带权并查集
有些题需要我们在点之间加上权值
并查集实际上就是在维护若干棵树,并查集的合并和查询优化,实际上就是在改变树的形状,把细长的树变成粗短的树。
# 带权值的路径压缩
定义 表示节点 到父节点的权值
在路径压缩时, 数组时累加关系,定义 时修改后的 数组,也就是 ,在具体的题目中,可能由相乘,异或等其他操作
# 带权值的合并
合并的操作中,把点 与点 合并,就是把 的祖先 合并到 的祖先 中,然后在 和 之间添加权值
具体怎么写要视情况而定