前置知识
可持久化数组
简介
先来一道模板题:
可持久化并查集
大致意思就是要你写一个数据结构,支持
-
合并a,b所在集合
-
退回到第k次操作之后的状态
-
查询a和b是不是在同一个集合里面
可以看到,除去第二个操作以外普通的并查集就可以解决
普通并查集一般是基于数组的,而可持久化并查集是基于可持久化数组的
话说这个东西名字听起来很高端,实际上实现起来其实不是很难(
并查集
相信都会写普通的并查集。。。但是这里有一点比较重要
普通的并查集最简单的优化是路径压缩和按秩合并(我比较菜所以一般只打路径压缩),就是这两种优化让并查集的时间复杂度变成近似于常数,但是可持久化并查集不能用路径压缩。因为路径压缩的过程会对fa数组进行修改,普通的数组改一改还ok,但是可持久化数组的每次更改都会添加一条链。久而久之整个并查集的空间复杂度就会起飞,所以我们主要使用按秩合并的思路。
按秩合并的话其实也不难,这里给出一份普通并查集的按秩合并代码:
void merge(int x,int y) {
x=find(x);
y=find(y);
if(x==y) return ;
if(dep[x]
就是在合并的时候只从深度小的往深度大的合并,然后深度一样的话就特判之后dep[x]++就好了,这样可以保证树高最高为log(n),也就是不存在链。
具体的原理。。。就自行百度吧,反正也不是很难(和启发式合并有点像?)
可持久化并查集
因为我们的优化是按秩合并,所以我们要维护两个可持久化数组(fa和dep)。当然,既然是两个数组,我们就要开两倍的内存空间:
struct node {
int l,r,sum;
} t[maxn*40*2];
然后我们通过开两个root数组来标记不同的数组的树根,这样就相当于开两个可持久化数组了,然后加上用来分配内存的计数器和一些题目里面的变量什么的
int n,m,tot,cnt,rootfa[maxn],rootdep[maxn];
接着,并查集一开始有一个给fa数组赋值的操作,fa[i]=i,所以我们在可持久化数里面也写一个build函数来完成这个工作
void build(int l,int r,int &now) {
now=++cnt;
if(l==r) {
t[now].sum=++tot;
return;
}
int mid=(l+r)/2;
build(l,mid,t[now].l);
build(mid+1,r,t[now].r);
}
这里的tot就是上面定义的,用来给叶子节点自增 ,应该还是蛮简单的
接下来就是一个可持久化数组的板子,这里就不再赘述了:
void modify(int l,int r,int ver,int &now,int pos,int num) {//ver指向历史版本,now指向当前节点
t[now=++cnt]=t[ver];
if(l==r) {
t[now].sum=num;
return;
}
int mid=(l+r)/2;
if(pos<=mid) modify(l,mid,t[ver].l,t[now].l,pos,num);
else modify(mid+1,r,t[ver].r,t[now].r,pos,num);
}
int query(int l,int r,int now,int pos) {
if(l==r) return t[now].sum;
int mid=(l+r)/2;
if(pos<=mid) return query(l,mid,t[now].l,pos);
else return query(mid+1,r,t[now].r,pos);
}
然后我们就来写find函数了。
find函数本身还是比较简单,但是要注意,我们不要路径压缩。原因前面已经讲了,这里就直接放代码:
int find(int ver,int x) {
int fx=query(1,n,rootfa[ver],x);
return fx==x?x:find(ver,fx);
}
然后我们先不急着说merge函数,我们先来看一下2操作是怎么实现的。
2操作是退回第k个版本,当然,如果我们有root数组的话,我们可以这么写:
rootfa[ver]=rootfa[x];
rootdep[ver]=rootdep[x];
ver指向当前版本,我们可以很简单地直接把x版本的root值复制过来,这样他们就都指向同一颗主席树,也就相当于把整个版本都复制过来。
然后就是merge函数,merge函数其实也不难,就照着普通按秩合并的代码一通乱改就好了:
void merge(int ver,int x,int y) {
x=find(ver-1,x);
y=find(ver-1,y);
if(x==y) {
rootfa[ver]=rootfa[ver-1];
rootdep[ver]=rootdep[ver-1];
} else {
int depx=query(1,n,rootdep[ver-1],x);
int depy=query(1,n,rootdep[ver-1],y);
if(depxdepy) {
modify(1,n,rootfa[ver-1],rootfa[ver],y,x);
rootdep[ver]=rootdep[ver-1];
} else {
modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
modify(1,n,rootdep[ver-1],rootdep[ver],y,depy+1);
}
}
}
关于为什么ver要-1的问题,因为个人代码的写法(其他很多dalao的板子里面没这个问题)问题,当我们的程序运行到merge之前(可以理解为你在merge那里打了一个断点,然后程序在这个断电处暂停时的状态),此时的ver虽然是指向着一个新的版本,但是这个版本内什么都没有,所以我们的merge函数需要的是root[ver-1]指向的版本内的值,所以要注意在merge函数里面每次modify和query时其所指向的历史版本都应该是ver-1(反正不管我们是查询还是退回最后都是在两个root数组后面修改)。然后就是要注意每次如果对dep数组没影响的时候就要把ver-1版本的dep数组给同步到ver版本里面去,反之,如果有修改的话就不要同步了。
同理,我们的find函数本来也是要ver-1的,但是因为query并没有涉及到任何数组的更改,所以我们直接在Main函数内的find之前:(后面的AC代码里面有写到)
rootfa[ver]=rootfa[ver-1];
rootdep[ver]=rootdep[ver-1];
可能现在你有一个疑惑,既然find可以这么搞,那么为什么merge不能也这么搞呢?
是这样的,我们回到可持久化数组的模板里面可以发现,在第一行里面就已经是一个给now+1的语句,并且这里的now传的还是引用。如果我们在merge之前写上这两句话,我们最终更改的就是rootfa[ver+2],这肯定是不对的,所以我们要手动给ver-1而不是直接复制整个root
下面给出模板题的AC代码:
#include
using namespace std;
const int maxn=1e6+10;
struct node {
int l,r,sum;
} t[maxn*40*2];
int n,m,tot,cnt,rootfa[maxn],rootdep[maxn];
void build(int l,int r,int &now) {
now=++cnt;
if(l==r) {
t[now].sum=++tot;
return;
}
int mid=(l+r)/2;
build(l,mid,t[now].l);
build(mid+1,r,t[now].r);
}
void modify(int l,int r,int ver,int &now,int pos,int num) {
t[now=++cnt]=t[ver];
if(l==r) {
t[now].sum=num;
return;
}
int mid=(l+r)/2;
if(pos<=mid) modify(l,mid,t[ver].l,t[now].l,pos,num);
else modify(mid+1,r,t[ver].r,t[now].r,pos,num);
}
int query(int l,int r,int now,int pos) {
if(l==r) return t[now].sum;
int mid=(l+r)/2;
if(pos<=mid) return query(l,mid,t[now].l,pos);
else return query(mid+1,r,t[now].r,pos);
}
int find(int ver,int x) {
int fx=query(1,n,rootfa[ver],x);
return fx==x?x:find(ver,fx);
}
void merge(int ver,int x,int y) {
x=find(ver-1,x);
y=find(ver-1,y);
if(x==y) {
rootfa[ver]=rootfa[ver-1];
rootdep[ver]=rootdep[ver-1];
} else {
int depx=query(1,n,rootdep[ver-1],x);
int depy=query(1,n,rootdep[ver-1],y);
if(depxdepy) {
modify(1,n,rootfa[ver-1],rootfa[ver],y,x);
rootdep[ver]=rootdep[ver-1];
} else {
modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
modify(1,n,rootdep[ver-1],rootdep[ver],y,depy+1);
}
}
}
int main(void) {
scanf("%d %d",&n,&m);
build(1,n,rootfa[0]);
for(int ver=1; ver<=m; ver++) {
int opt,x,y;
scanf("%d",&opt);
if(opt==1) {
scanf("%d %d",&x,&y);
merge(ver,x,y);
} else if(opt==2) {
scanf("%d",&x);
rootfa[ver]=rootfa[x];
rootdep[ver]=rootdep[x];
} else {
scanf("%d %d",&x,&y);
rootfa[ver]=rootfa[ver-1];
rootdep[ver]=rootdep[ver-1];
int fx=find(ver,x),fy=find(ver,y);
printf("%d\n",fx==fy?1:0);
}
}
}
BTW,如果要维护集合的属性的化(比如说是集合大小之类的?),就要用到可持久化带权并查集
其实也非常简单,就是在可持久化数组里面新开一个变量去维护