可持久化并查集
可持久化并查集
前言
- 听名字像是一个十分高端的东西,在今年NOI2018之前,我从未想过自己会用这个数据结构
- 然而,当发现Day1 T1用可持久化并查集可以暴力A的时候,心中无尽的无奈......
(毕竟不会) - 考完后了解了一下,发现似乎是一个挺好理解的数据结构。
- 所以就写了这篇学习笔记!
前置技能
- 可持久化并查集,所需要知道的前置技能很显然!
- 顾名思义,可持久化并查集=可持久化+并查集=可持久化数组+并查集=主席树+并查集!
- 因此,我们首先要会主席树和并查集。
- 可持久化数组这个没什么好说的,就那几个操作,详情见洛谷可持久化数组模板
- 并查集倒是要提一下!
- 并查集中有几种合并方式:
- 一种是直接暴力连父亲
(这显然用不上); - 一种是路径压缩的合并(这个在普通并查集中很常用,但是好像无法在可持久化并查集中用,听说是可以构造数据使可持久化并查集的空间爆掉?);
- 还有一种是按秩合并,也就是可持久化并查集中常用的合并方式!其实也就是一种类似于启发式合并的方式,每一次合并时选择一个深度小的点向深度大的合并。这样就可以保证并查集的高度不会增长的太快,保证高度尽量均衡。
步入正题——可持久化并查集
- 其实我们可以发现看懂了前置技能后,可持久化并查集已经不难实现。
- 可持久化并查集其实就是指的用可持久化数组维护并查集中的\(Fa\)与按秩合并所需要的\(dep\)
- 所谓可持久化并查集,可以进行的操作就只有几个:
- 回到历史版本
(不然怎么叫可持久化呢2333) - 合并两个集合
(毕竟还是个并查集么) - 查询节点所在集合的祖先,当然,因此也可以判断是否在同一个集合中!
- 对于1操作,我们可以很轻松的利用可持久化数组实现。就直接把当前版本的根节点定为第k个版本的根节点就行了!
- 至于代码实现?
root[i]=root[x];
//是不是很简单呀!
- 对于2操作,其实也就是按照我在前置技能中所说的按秩合并!
- 对于3操作,也就是在可持久化数组中查询!
- 这样说肯定会有点懵圈,不如一个个函数的解释!
#define Mid ((l+r)>>1)
#define lson L[rt],l,Mid
#define rson R[rt],Mid+1,r
// 整个代码的三个宏定义
初始化建树
void build(int &rt,int l,int r)
{
rt=++cnt;
if(l==r){fa[rt]=l;return ;}
build(lson);build(rson);
}
// 就是普通的可持久化数组构建法,不过维护的是Fa而已
合并
void merge(int last,int &rt,int l,int r,int pos,int Fa)
{
rt=++cnt;L[rt]=L[last],R[rt]=R[last];
if(l==r)
{
fa[rt]=Fa;
dep[rt]=dep[last];//继承上个版本的值
return ;
}
if(pos<=Mid)merge(L[last],lson,pos,Fa);
else merge(R[last],rson,pos,Fa);
}
// 这个就是单纯的将一个点合并到另一个点上的可持久化数组操作!
修改节点深度(方便按秩合并)
void update(int rt,int l,int r,int pos)
{
if(l==r){dep[rt]++;return ;}
if(pos<=Mid)update(lson,pos);
else update(rson,pos);
}
// 可持久化数组普通操作
// 可能有人会问为什么修改节点深度的时候不需要新开节点!
// 其实新开节点是根据我们的需要来的!
// 如果我们需要某个值在某个版本的信息,那么,每当这个值进行修改的时候,我们都需要新添加一个节点,使得我们可以查到各个版本的值
// 然而dep我们并不需要知道它以前的值是多少,我们只需要用它当前的值去合并就行了!
查询某一个值所在可持久化数组中的下标
int query(int rt,int l,int r,int pos)
{
if(l==r)return rt;
if(pos<=Mid)return query(lson,pos);
else return query(rson,pos);
}
// 为了找祖先的操作
查找祖先
int find(int rt,int pos)
{
int now=query(rt,1,n,pos);
if(fa[now]==pos)return now;
return find(rt,fa[now]);
}
// 暴力找祖先
- 以上操作就是可持久化并查集的基础函数 单次操作复杂度均为\(log\)级的,空间需要注意,也要开\(n*log\)级,一般就正常空间乘上\(40\)左右吧。
- 合并与查询操作就和普通并查集差不多,只是需要注意当前查询的版本是什么就可以了。
- 当然还需要注意一点,每一次操作都要先把上个版本给传递过来\(root[i]=root[i-1]\)
- 放个代码看看吧!
- 按秩合并
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]!=fa[posy])
{
if(dep[posx]>dep[posy])swap(posx,posy);
merge(root[i-1],root[i],1,n,fa[posx],fa[posy]);
if(dep[posx]==dep[posy])update(root[i],1,n,fa[posy]);
// 因为不可能出现深度相同的两个点,所以要把其中一个点深度+1,由于是深度小的合到深度大的上,所以把深度小的增加深度
}
- 查找
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]==fa[posy])puts("1");
else puts("0");
// 这个真和普通并查集没区别,只是需要注意是什么版本的并查集...
- 至此,可持久化并查集的所有操作就已经解释完了!
(相信聪明的你一定可以实现它)
其实,把上面的操作拼起来就是完整代码,不过我还是粘一个完整版吧!
#include<bits/stdc++.h>
#define N 301000
using namespace std;
template<typename T>inline void read(T &x)
{
x=0;
static int p;p=1;
static char c;c=getchar();
while(!isdigit(c)){if(c=='-')p=-1;c=getchar();}
while(isdigit(c)) {x=(x<<1)+(x<<3)+(c-48);c=getchar();}
x*=p;
}
int n,m;
int L[N*30],R[N*30],fa[N*30],dep[N*30];
int root[N*30];
namespace Persistant_Union_Set
{
#define Mid ((l+r)>>1)
#define lson L[rt],l,Mid
#define rson R[rt],Mid+1,r
int cnt;
void build(int &rt,int l,int r)
{
rt=++cnt;
if(l==r){fa[rt]=l;return ;}
build(lson);build(rson);
}
void merge(int last,int &rt,int l,int r,int pos,int Fa)
{
rt=++cnt;L[rt]=L[last],R[rt]=R[last];
if(l==r)
{
fa[rt]=Fa;
dep[rt]=dep[last];
return ;
}
if(pos<=Mid)merge(L[last],lson,pos,Fa);
else merge(R[last],rson,pos,Fa);
}
void update(int rt,int l,int r,int pos)
{
if(l==r){dep[rt]++;return ;}
if(pos<=Mid)update(lson,pos);
else update(rson,pos);
}
int query(int rt,int l,int r,int pos)
{
if(l==r)return rt;
if(pos<=Mid)return query(lson,pos);
else return query(rson,pos);
}
int find(int rt,int pos)
{
int now=query(rt,1,n,pos);
if(fa[now]==pos)return now;
return find(rt,fa[now]);
}
#undef Mid
#undef lson
#undef rson
}
using namespace Persistant_Union_Set;
int main()
{
read(n);read(m);
build(root[0],1,n);
for(int i=1;i<=m;i++)
{
static int opt,x,y;
read(opt);read(x);
if(opt==1)
{
read(y);
static int posx,posy;
root[i]=root[i-1];
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]!=fa[posy])
{
if(dep[posx]>dep[posy])swap(posx,posy);
merge(root[i-1],root[i],1,n,fa[posx],fa[posy]);
if(dep[posx]==dep[posy])update(root[i],1,n,fa[posy]);
}
}
else if(opt==2)root[i]=root[x];
else if(opt==3)
{
read(y);
root[i]=root[i-1];
static int posx,posy;
posx=find(root[i],x);posy=find(root[i],y);
if(fa[posx]==fa[posy])puts("1");
else puts("0");
}
}
return 0;
}
扩展——可持久化带权并查集
- 感觉这个比普通的带权并查集直接一些!
- 直接在可持久化数组里维护,即在合并父亲的时候同时维护权值的信息就行了!
(是不是特别的简单呢OVO )
题目
- 可持久化并查集的题目我还真没做过几个,毕竟这个东西只要板子会打,剩下的都是思维的事情了,代码实现的难度并不高。题目好像也没有几个。
- 洛谷的模板题可以打一下,练一练板子。
(以后好复制) - 如果实在想练一下,那么就去把noi2018归程用可持久化并查集给做掉233.