可持久化数据结构
可持久化数据结构呢,就是说这些数据结构,它们都非常持久(
其实就是可以访问和修改历史版本的信息
可持久化线段树
可持久化权值线段树就是主席树
如果你还不太了解,可以看看
当然还有更普遍的可持久化线段树——支持区间修改的。考虑pushdown会影响下方历史版本的线段树信息,自然想到标记永久化。就是不用pushdown或pushup,每次把修改操作的tag打到一个完整覆盖的区间上,并且在其途径的节点上直接修改sum。查询的时候返回被完整覆盖到的区间 t[id].sum+途径的tag之和*(t[id].r-t[id].l+1) 的和。
Q:树套树不是单点修改吗(比区间修改要求更少)?为什么不可以用标记永久化?
A:很显然标记永久化处理的是维护区间的线段树,且每次修改都新建一个版本。而树套树由于询问要求,必须建权值线段树,这样对于修改来说,就相当于在中间的版本进行操作,还会影响到后面的版本,就必须用树套树了。
[可持久化线段树区间修改模板]To the moon
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5;
int n,m,a[maxn],rt[maxn],cnt,now;
struct node{
int lc,rc;
ll v,tg;
}t[maxn<<6];
void build(int &x,int l,int r){
x=++cnt;
if(l==r) return t[x].v=a[l],void();
int mid=(l+r)>>1;
build(t[x].lc,l,mid);
build(t[x].rc,mid+1,r);
t[x].v=t[t[x].lc].v+t[t[x].rc].v;
}
void add(int &x,int y,int l,int r,int vl,int vr,int val){
x=++cnt,t[x]=t[y];
t[x].v+=1ll*val*(min(r,vr)-max(l,vl)+1);//
if(vl<=l&&r<=vr) return t[x].tg+=val,void();
int mid=(l+r)>>1;
if(vl<=mid) add(t[x].lc,t[y].lc,l,mid,vl,vr,val);
if(vr>mid) add(t[x].rc,t[y].rc,mid+1,r,vl,vr,val);
}
ll query(int x,int l,int r,int vl,int vr,ll tg){
if(vl<=l&&r<=vr) return t[x].v+tg*(r-l+1);//
int mid=(l+r)>>1; tg+=t[x].tg; ll res=0;
if(vl<=mid) res=query(t[x].lc,l,mid,vl,vr,tg);
if(vr>mid) res+=query(t[x].rc,mid+1,r,vl,vr,tg);
return res;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
build(rt[0],1,n);
while(m--){
char c; int l,r,t;
scanf(" %c",&c);
if(c=='C'){
scanf("%d%d%d",&l,&r,&t);
add(rt[now+1],rt[now],1,n,l,r,t),now++;
}
else if(c=='Q'){
scanf("%d%d",&l,&r);
printf("%lld\n",query(rt[now],1,n,l,r,0));
}
else if(c=='H'){
scanf("%d%d%d",&l,&r,&t);
printf("%lld\n",query(rt[t],1,n,l,r,0));
}
else{
scanf("%d",&t);
now=t;
}
}
return 0;
}
神秘例题:七彩树
根据可持久化线段树的套路,一般都是对于一维建可持久化线段树,另一位在线段树内部维护。
本题,我们发现如果按照数颜色问题最常见套路,记录该颜色上一次出现的位置去做,会把我们二维的题变成三维的,完全不可行。考虑更直接的思路,对当前颜色(上一次出现的位置+1)到(当前位置)的不同颜色个数要整体+1,用树上差分维护。由于树上差分的存在,那么我们就只能以深度范围为轴建立可持久化线段树,每一棵线段树维护当前深度的点对整棵树dfs序上对应点的贡献。
七彩树
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+5;
int T,n,m,h[maxn],to[maxn],nxt[maxn],cnt,f[maxn][21],dep[maxn],dfn[maxn],num;
int lst,id[maxn],tot,siz[maxn],a[maxn],rt[maxn],idx[maxn],clo;
struct node{
int lc,rc,v;
}t[maxn<<5];
set<int>s[maxn];
map<int,int>mp;
void addedge(int u,int v){
nxt[++cnt]=h[u];
to[cnt]=v;
h[u]=cnt;
}
void init(){
for(int i=1;i<=n;i++){
h[i]=to[i]=nxt[i]=rt[i]=0;
s[i].clear();
for(int j=0;j<=20;j++) f[i][j]=0;
}
for(int i=1;i<=tot;i++) t[i].lc=t[i].rc=t[i].v=0;
mp.clear();
lst=cnt=num=tot=clo=0;
}
void pre(int x,int fa){
f[x][0]=fa,siz[x]=1;
dep[x]=dep[fa]+1;
dfn[x]=++num,idx[num]=x;
for(int i=1;i<=20;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(y!=fa) pre(y,x),siz[x]+=siz[y];
}
}
int lca(int x,int y){
if(dep[x]<dep[y]) swap(x,y);
for(int i=20;i>=0;i--){
if(dep[f[x][i]]>=dep[y]) x=f[x][i];
}
if(x==y) return x;
for(int i=20;i>=0;i--){
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
}
return f[x][0];
}
bool cmp(int x,int y){
return dep[x]<dep[y];
}
void add(int &x,int l,int r,int pos,int val){
if(!x) x=++tot; t[x].v+=val;
if(l==r) return ;
int mid=(l+r)>>1;
if(pos<=mid) add(t[x].lc,l,mid,pos,val);
else add(t[x].rc,mid+1,r,pos,val);
}
int merge(int x,int y){
if(!x||!y) return x+y;
t[x].v+=t[y].v;
t[x].lc=merge(t[x].lc,t[y].lc);
t[x].rc=merge(t[x].rc,t[y].rc);
return x;
}
int query(int x,int y,int l,int r,int vl,int vr){
if(vl<=l&&r<=vr) return t[x].v-t[y].v;
int mid=(l+r)>>1,res=0;
if(vl<=mid) res=query(t[x].lc,t[y].lc,l,mid,vl,vr);
if(vr>mid) res+=query(t[x].rc,t[y].rc,mid+1,r,vl,vr);
return res;
}
int main(){
scanf("%d",&T);
while(T--){
init();
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
if(!mp[a[i]]) mp[a[i]]=++clo;
a[i]=mp[a[i]];
}
for(int i=2,fa;i<=n;i++){
scanf("%d",&fa);
addedge(fa,i);
}
pre(1,0);
for(int i=1;i<=n;i++) id[i]=i;
sort(id+1,id+n+1,cmp);
int now=0;
for(int i=1;i<=n;i++){
if(dep[id[i]]!=dep[id[i-1]]) now++;
int c=a[id[i]],x=id[i],lc=0;
if(s[c].size()){
auto it=s[c].lower_bound(dfn[x]);
if(it!=s[c].end()){
// printf("%d\n",(*it));
int p=lca(idx[(*it)],x);
lc=p;
}
if(it!=s[c].begin()){
it--;
// printf("%d\n",(*it));
int p=lca(idx[(*it)],x);
if(dep[lc]<dep[p]) lc=p;
}
}
s[c].insert(dfn[x]);
if(lc) add(rt[now],1,n,dfn[lc],-1);
add(rt[now],1,n,dfn[x],1);
// printf("do: %d %d %d\n",now,x,lc);
}
for(int i=1;i<=now;i++) rt[i]=merge(rt[i],rt[i-1]);
while(m--){
int x,d;
scanf("%d%d",&x,&d);
x^=lst,d^=lst,d=min(d,now-dep[x]);
// printf("%d %d %d %d %d\n",x,d,dfn[x],siz[x],dep[x]);
printf("%d\n",lst=query(rt[dep[x]+d],rt[dep[x]-1],1,n,dfn[x],dfn[x]+siz[x]-1));
}
}
return 0;
}
可持久化平衡树
一般使用的都是可持久化FHQ-treap,代码短,常数小,操作简单,容易理解(splay因存在旋转操作,不能可持久化
(实际上是因为我平衡树只会FHQ-treap
前置:FHQ_treap,即无旋treap
treap是维护了一个二元组,其中val为权值,prio(priority)为一个随机的优先级。其中val满足二叉搜索树性质(中序遍历结果是val从小到大的排序),prio满足(小根或大根)堆的性质。
实际上,treap就是依靠这个随机化赋予的权值,打乱节点的插入顺序,保证树的形态不会太差,高度大概在logn左右,从而保证各种操作的期望复杂度是O(logn)
split(分裂) :两种分裂方式,按排名或按值。p表示当前子树根节点,v表示以v为分界线分割树,x,y表示分裂之后左右子树的根。注意,当根为0时,一定要清空x,y,否则死循环!!!
merge(合并) :实际上只需要按照prio的值决定父子节点关系,然后类似于线段树合并去做就行。注意这里合并的时候一定要按照权值顺序合并,因为我们在merge函数里只考虑了优先级,没有考虑权值的问题。
添加值操作,实际上就是按值分裂开,然后再把新建的点和它们合并到一起(跟拼拼图似的是吧
删除值操作类似,把区间按值拆开,把中间一段去掉再合并就行
查询排名为k的值,直接按二叉搜索树的性质递归即可
前后继和排名以此类推
空间O(n),时间O(nlogn)
[模板]普通平衡树
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int root,num,n;
struct tree{
int l,r,val,tmp,size;
}t[maxn];//val权值,tmp随机值
void newnode(int &x,int v){
x=++num;
t[x].val=v;
t[x].tmp=rand();
t[x].size=1;
}
void pushup(int p){
t[p].size=t[t[p].l].size+t[t[p].r].size+1;
}
void split(int p,int v,int &x,int &y){//分裂维护顺序
if(!p){//注意清空!
x=y=0;
return ;
}
if(t[p].val<=v){
x=p;
split(t[x].r,v,t[x].r,y);
pushup(x);
}
else{
y=p;
split(t[y].l,v,x,t[y].l);
pushup(y);
}
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(t[x].tmp<t[y].tmp){//依靠随机数
t[x].r=merge(t[x].r,y);
pushup(x); return x;
}
else{
t[y].l=merge(x,t[y].l);
pushup(y); return y;
}
}
void insert(int v){
int x,y,z;
split(root,v,x,y);
newnode(z,v);
root=merge(merge(x,z),y);
}
void del(int v){
int x,y,z;
split(root,v,x,z);
split(x,v-1,x,y);
y=merge(t[y].l,t[y].r);
root=merge(merge(x,y),z);
}
int qnum(int v){
int x,y;
split(root,v-1,x,y);
int ans=t[x].size+1;
root=merge(x,y);
return ans;
}
int query(int root,int v){
if(v==t[t[root].l].size+1) return t[root].val;
else if(v<=t[t[root].l].size) return query(t[root].l,v);
else return query(t[root].r,v-t[t[root].l].size-1);
}
int pre(int v){
int x,y,s,ans;
split(root,v-1,x,y);
s=t[x].size;
ans=query(x,s);
root=merge(x,y);
return ans;
}
int nxt(int v){
int x,y,ans;
split(root,v,x,y);
ans=query(y,1);
root=merge(x,y);
return ans;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int tmp,v;
scanf("%d%d",&tmp,&v);
if(tmp==1) insert(v);
else if(tmp==2) del(v);
else if(tmp==3) printf("%d\n",qnum(v));
else if(tmp==4) printf("%d\n",query(root,v));
else if(tmp==5) printf("%d\n",pre(v));
else printf("%d\n",nxt(v));
}
return 0;
}
可持久化普通平衡树
和普通平衡树的区别其实有且仅有每次分裂和合并时不覆盖(直接使用)之前的节点,而是新建节点。因为这样才能保证历史信息一直存在,方便后面调用。
注意空间要开nlogn!!!因为每次操作都新建节点!
可持久化普通平衡树
#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+5;
const int inf=(1ll<<31)-1;
int n,rt[maxn],num;
struct node{
int lc,rc,siz,v,prio;
}t[maxn<<6];
int newnode(int v){
t[++num].siz=1;
t[num].v=v;
t[num].prio=rand();
return num;
}
void pushup(int x){
t[x].siz=t[t[x].lc].siz+t[t[x].rc].siz+1;
}
void split(int p,int v,int &x,int &y){
if(!p){
x=y=0; return ;
}
if(t[p].v<=v){
x=newnode(0); t[x]=t[p];
split(t[x].rc,v,t[x].rc,y);
pushup(x);
}
else{
y=newnode(0); t[y]=t[p];
split(t[y].lc,v,x,t[y].lc);
pushup(y);
}
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(t[x].prio<t[y].prio){
int p=newnode(0); t[p]=t[x];
t[p].rc=merge(t[p].rc,y);
pushup(p); return p;
}
else{
int p=newnode(0); t[p]=t[y];
t[p].lc=merge(x,t[p].lc);
pushup(p); return p;
}
}
int kth(int x,int v){
if(t[t[x].lc].siz+1==v) return t[x].v;
if(v<=t[t[x].lc].siz) return kth(t[x].lc,v);//搞笑,这儿写错了、、
else return kth(t[x].rc,v-t[t[x].lc].siz-1);
}
int read(){
char ch=getchar(); int f=1,x=0;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar('0'+x%10);
}
int main(){
srand(time(0));
n=read();
for(int i=1;i<=n;i++){
int p=read(),opt=read(),v=read(),x,y,z;
rt[i]=rt[p];
if(opt==1){//insert
split(rt[i],v,x,y);
rt[i]=merge(x,merge(newnode(v),y));
}
else if(opt==2){//del
split(rt[i],v-1,x,y);
split(y,v,z,y);
z=merge(t[z].lc,t[z].rc);
rt[i]=merge(x,merge(z,y));
}
else{
if(opt==3){//qnum
split(rt[i],v-1,x,y);
write(t[x].siz+1);
rt[i]=merge(x,y);
}
else if(opt==4){//kth
write(kth(rt[i],v));
}
else if(opt==5){//pre
split(rt[i],v-1,x,y);
if(!t[x].siz) write(-inf);
else write(kth(x,t[x].siz));
rt[i]=merge(x,y);
}
else{//nxt
split(rt[i],v,x,y);
if(!t[y].siz) write(inf);
else write(kth(y,1));
rt[i]=merge(x,y);
}
putchar('\n');
}
}
return 0;
}
可持久化文艺平衡树
一般来说,线段树是用来维护区间的,平衡树是用来维护权值的。那么既然有权值线段树,就会有区间平衡树。
区间平衡树能实现线段树实现不了的东西,比如翻转区间。
和可持久化普通平衡树一样,每次分裂合并新建节点,再加一个表示翻转的tag就行。每次先pushdown再操作。下传时给左右儿子打上标记并交换。
可持久化文艺平衡树
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+5;
int n,rt[maxn],num;
ll lst;
struct node{
int lc,rc,siz,prio,v,tg;
long long sum;
}t[maxn<<6];
int newnode(int v){
t[++num].siz=1;
t[num].v=t[num].sum=v;//平衡树挂分指南:初始化不完全!
t[num].prio=rand();
return num;
}
int cpy(int x){
int y=newnode(0);
t[y]=t[x];
return y;
}
void pushup(int x){
t[x].siz=t[t[x].lc].siz+t[t[x].rc].siz+1;
t[x].sum=t[t[x].lc].sum+t[t[x].rc].sum+t[x].v;
}
void pushdown(int x){
if(!t[x].tg) return ;
if(t[x].lc) t[x].lc=cpy(t[x].lc);
if(t[x].rc) t[x].rc=cpy(t[x].rc);
swap(t[x].lc,t[x].rc);
if(t[x].lc) t[t[x].lc].tg^=1;
if(t[x].rc) t[t[x].rc].tg^=1;
t[x].tg=0;
}
void split(int p,int v,int &x,int &y){
if(!p){
x=y=0; return ;
}
pushdown(p);
if(v>t[t[p].lc].siz){//这里想清楚,包含了v==t[t[p].lc].siz+1的情况,此时我们希望这个节点归于x子树
x=cpy(p);
split(t[x].rc,v-t[t[p].lc].siz-1,t[x].rc,y);
pushup(x);
}
else{
y=cpy(p);
split(t[y].lc,v,x,t[y].lc);
pushup(y);
}
}
int merge(int x,int y){
if(!x||!y) return x+y;
pushdown(x),pushdown(y);
if(t[x].prio<t[y].prio){
int p=newnode(0); t[p]=t[x];
t[p].rc=merge(t[p].rc,y);
pushup(p); return p;
}
else{
int p=newnode(0); t[p]=t[y];
t[p].lc=merge(x,t[p].lc);
pushup(p); return p;
}
}
ll read(){
char ch=getchar(); ll f=1,x=0;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar('0'+x%10);
}
int main(){
srand(time(0));
n=read();
for(int i=1;i<=n;i++){
int p=read(),opt=read(),x,y,z,l=read()^lst,r;
rt[i]=rt[p];
if(opt==1){
r=read()^lst;
split(rt[i],l,x,y);
rt[i]=merge(x,merge(newnode(r),y));
}
else if(opt==2){
split(rt[i],l-1,x,y);
split(y,1,z,y);
rt[i]=merge(x,y);
}
else if(opt==3){
r=read()^lst;
split(rt[i],l-1,x,y);
split(y,r-l+1,z,y);
t[z].tg^=1;
rt[i]=merge(x,merge(z,y));
}
else{
r=read()^lst;
split(rt[i],l-1,x,y);
split(y,r-l+1,z,y);
lst=t[z].sum;
rt[i]=merge(x,merge(z,y));
printf("%lld\n",lst);
}
}
return 0;
}
可持久化trie
说白了跟主席树(可持久化线段树)一样,每次先复制上一个节点,再插入当前即可(相当于做前缀和)。这里写成取地址的递归建树会显得更像主席树,个人觉得更好看(
查询也类似主席树。就是在01trie中查询某数和某段区间中数的异或最大值,先看能不能取到相反的字符,如果(相减后得到的)这段区间中有相反字符,加入贡献并走这边;否则走另一边。
有时写着写着代码可能会觉得有点奇怪、和主席树不那么一样,是因为字典树的信息存在边上,每条转移边代表了一个字符。稍加注意即可。
[HEOI2013] ALO
//这题二分+线段树 log^2就能过
#include<bits/stdc++.h>
#define lid (id<<1)
#define rid (id<<1|1)
using namespace std;
const int maxn=5e4+5;
int n,rt[maxn],a[maxn],cnt,tr[maxn<<2],ans;
struct node{
int c[2],siz;
}t[maxn*40];
void insert(int &p,int lst,int dep,int v){
p=++cnt,t[p]=t[lst],t[p].siz++;
if(dep<0) return ;
int x=(v>>dep)&1;
insert(t[p].c[x],t[lst].c[x],dep-1,v);
}
int query(int p,int lst,int dep,int v){
if(dep<0) return 0;
int x=(v>>dep)&1;
if(t[t[p].c[!x]].siz>t[t[lst].c[!x]].siz)
return query(t[p].c[!x],t[lst].c[!x],dep-1,v)+(1<<dep);
else return query(t[p].c[x],t[lst].c[x],dep-1,v);
}
void build(int id,int l,int r){
if(l==r) return tr[id]=a[l],void();
int mid=(l+r)>>1;
build(lid,l,mid),build(rid,mid+1,r);
tr[id]=max(tr[lid],tr[rid]);
}
int qmax(int id,int l,int r,int vl,int vr){
if(vl<=l&&r<=vr) return tr[id];
int mid=(l+r)>>1,res=0;
if(vl<=mid) res=qmax(lid,l,mid,vl,vr);
if(vr>mid) res=max(res,qmax(rid,mid+1,r,vl,vr));
return res;
}
int check1(int l,int r,int now){
int mid,ret=0,lim=r;
if(r<1||r<l) return ret;
while(l<=r){
mid=(l+r)>>1;
if(qmax(1,1,n,mid,lim)>=now){
ret=mid;
l=mid+1;
} else r=mid-1;
}
return ret;
}
int check2(int l,int r,int now){
int mid,ret=n+1,lim=l;
if(l>n||r<l) return ret;
while(l<=r){
mid=(l+r)>>1;
if(qmax(1,1,n,lim,mid)>=now){
ret=mid;
r=mid-1;
} else l=mid+1;
}
return ret;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
insert(rt[i],rt[i-1],31,a[i]);
}
build(1,1,n);
for(int i=1;i<=n;i++){
int l1=check1(1,i-1,a[i]),r1=check2(i+1,n,a[i]),l2=check1(1,l1-1,a[i]),r2=check2(r1+1,n,a[i]);
// printf("%d %d %d %d %d\n",l2,l1,i,r1,r2);
if(r1<=n) ans=max(ans,query(rt[r2-1],rt[l1],31,a[i]));
if(l1) ans=max(ans,query(rt[r1-1],rt[l2],31,a[i]));
}
printf("%d",ans);
return 0;
}
非递归写法
int insert(int lst,string st){
int ret=++cnt,p=cnt;
for(int i=0;i<st.size();i++){
int x=st[i]-'a';
tr[p]=tr[lst],tr[p].siz++;
tr[p].c[x]=++cnt;//这里必须新建节点
p=tr[p].c[x],lst=tr[lst].c[x];
}
tr[p].siz=tr[lst].siz+1;//注意细节,是要减的
return ret;
}
int query(int x,int y,int g){
for(int i=0;i<t.size();i++){
int p=t[i]-'a';
x=tr[x].c[p],y=tr[y].c[p],g=tr[g].c[p];
}
return tr[x].siz+tr[y].siz-tr[g].siz*2;
}
可持久化01trie挂分指南:(别问我怎么知道的
1)空间开小。因为每次在insert时,除了每个数位插入,root还要占用空间。
2)边界处理不清。如果用01trie维护异或和的问题,需要把异或和转化为前缀和去做,然而01trie本身就是一个前缀和,因此边界上有时会产生形如l-2的东西(一定要想清楚该不该-1!!!)。原因就是,可能直接从最开始选(l=1时),那么此时前缀和就应该-pre[0],而正常来说,trie从1开始维护。所以需要提前加入rt[0]这个点,访问到时进行特判。一车细节。
可持久化并查集
考虑可持久化线段树维护的东西,实际上就是可持久化数组
那么并查集本质上就是维护了fa数组和rk/siz数组。注意这里不能用路径压缩的原因是,路径压缩的时间复杂度是均摊的,如果构造数据,使它每次返回到那个需要修改一堆节点的操作,时空复杂度就都假了。
直接用可持久化线段树维护fa和rk数组就行,实现和普通并查集一样。想清楚,访问数组中的元素,需要在线段树中拿对应下标去查。
可持久化并查集
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m,cnt,rt[maxn];
struct node{
int lc,rc,fa,dep;
}t[maxn<<5];
void build(int &x,int l,int r){
x=++cnt;
if(l==r) return t[x].fa=l,void();
int mid=(l+r)>>1;
build(t[x].lc,l,mid);
build(t[x].rc,mid+1,r);
}
void add(int &x,int l,int r,int pos){
t[++cnt]=t[x],x=cnt;
if(l==r) return t[x].dep++,void();
int mid=(l+r)>>1;
if(pos<=mid) add(t[x].lc,l,mid,pos);
else add(t[x].rc,mid+1,r,pos);
}
void change(int &x,int l,int r,int pos,int fa){
t[++cnt]=t[x],x=cnt;
if(l==r) return t[x].fa=fa,void();
int mid=(l+r)>>1;
if(pos<=mid) change(t[x].lc,l,mid,pos,fa);
else change(t[x].rc,mid+1,r,pos,fa);
}
int query(int x,int l,int r,int pos){
if(l==r) return x;
int mid=(l+r)>>1;
if(pos<=mid) return query(t[x].lc,l,mid,pos);
else return query(t[x].rc,mid+1,r,pos);
}
int find(int root,int x){
int g=query(root,1,n,x);//数组中第x个数的fa为x,即根
if(t[g].fa==x) return g;
return find(root,t[g].fa);
}
void merge(int i,int x,int y){
x=find(rt[i],x),y=find(rt[i],y);
if(x==y) return ;
if(t[x].dep>t[y].dep) swap(x,y);
change(rt[i],1,n,t[x].fa,t[y].fa);//f[find(x)]=find(y)
if(t[x].dep==t[y].dep) add(rt[i],1,n,t[y].fa);//按秩合并
}
void check(int i,int x,int y){
x=find(rt[i],x),y=find(rt[i],y);
if(t[x].fa==t[y].fa) printf("1\n");
else printf("0\n");
}
int main(){
scanf("%d%d",&n,&m);
build(rt[0],1,n);
for(int i=1;i<=m;i++){
int opt,x,y;
scanf("%d%d",&opt,&x);
rt[i]=rt[i-1];
if(opt==1){
scanf("%d",&y);
merge(i,x,y);
}
else if(opt==2) rt[i]=rt[x];
else{
scanf("%d",&y);
check(i,x,y);
}
}
return 0;
}
本文作者:YYYmoon
本文链接:https://www.cnblogs.com/YYYmoon/p/18651957
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步