P3690 【模板】动态树(LCT)

P3690 【模板】动态树(LCT)

闲话:

余既知 LCT ,后半日,吾志学之。时至机房,广查博客,或苦思冥想。怎料实力不济,铩羽而归。他人问之:“闻汝知 LCT ,且何谓 LCT 也”。其后半日,吾弃之,树坏不修。其后半年,余久摆烂无聊,乃复修LCT,其成稍进于前。然自后余多爱线段树,不常写。

blog 有 LCT 树,吾集训之年所首知也,今已亭亭如盖矣。

人话版:

暑假时第一次知道了 LCT 奈何蒟蒻太弱,三看题解而不懂。然后就搁置了大半年,然后在 2024/12/23 靠着神秘的题解之力 A 了这题,但还是一道 LCT 除模板以外的题都没写,直至今日,再战LCT,复修总结。

Solution:

首先是我最赞扬的一篇题解,要看图的话可以参考,这篇题解的图真的很好。

splay的部分这里先不过多叙述,我们先把它当成普通的splay就好。支持两种操作

  • 1 rotate :将某个节点上旋
  • 2 splay :将某个节点不断上旋,使其成为该平衡树的根。

首先我们明确一下,LCT 维护的是很多颗平衡树,每一颗平衡树对应的是一个联通块,平衡树上的键值是在原图上该节点的深度

首先介绍一下最重要的操作:

access

表示将当前节点xx所在连通块(平衡树)的根root 这段路上的所有节点(在原树上)拿出来建一颗平衡树。

虽然说是“拿出来”,但是我们实际的操作是将从 x 开始不断跳父亲,让后将当前节点所对应平衡树节点 x 设为 其父亲 fa 的右儿子。我们思考为什么这么做是可行的,前面说到:平衡树上的键值是在原图上该节点的深度。所以在合并操作进行到 fa 时,并没有比深度更大的节点,所以 fa 的左儿子其实是空的。所以我们只要将之前合并好的连通块作为 fa 的右儿子就好了。这样就保证了在进行完 access 操作之后,x , root 在同一平衡树内并且这颗平衡树有且仅有 x->root 这跳路径上的所有节点。

形式化的: splayx= y|y(x>root)

说了那么多,其实代码很简单:

void access(int x)
{
int y=0;
while(x)
{
//每次 splay 完,x 是没有左子树的,因为在目前合并出来的这颗 平衡树上,x 在原树中的深度最小。
//而它的右儿子又被我们强行赋为了 rt->x 这条链所形成的平衡树
//这样就保证了在 access 完了之后,这颗平衡树维护只存在 x->root 这段路径上的点
splay(x);rs=y;pushup(x);
y=x;x=fa;
}
}

有了这个操作剩下的操作就好理解了。

make_root:

使得 x 成为当前平衡树的根。注意是成为而非上旋至。这两个操作有着本质上的区别。make_root会直接把它在原树上的深度直接赋为当前平衡树(连通块)内最小的,使得该节点在平衡树和原树都是该平衡树(连通块)真正意义上的根。

听起来貌似挺麻烦的,但其实非常好写。

inline void rev(int x)
{
swap(t[x].ch[0],t[x].ch[1]);
t[x].tag^=1;
}
void make_root(int x)//换根
{
access(x);splay(x);
rev(x);
}

为什么可以这么写:因为在 access(x),splay(x) 执行完了之后 x 是根而且没有右子树。那么我们将 x rev了之后 x没有左子树了。也就是说,x 从该平衡树下最深的点变成最浅的点了,即该连通块的根。

剩下的操作都真·很简单了,说明都在注释代码里了。

int find(int x)//找到 x 所在的 平衡树/联通块的根 (动态的树,有可能是一个森林,所有节点的根并不统一)
{
access(x);splay(x);
while(ls)pushdown(x),x=ls;
splay(x);
return x;
}
void splite(int x,int y)//我也不知道这个函数为什么叫 splite 明明就是在把 x->y 这条路径找出来好吧
{
make_root(x);
access(y);splay(y);
}
void link(int x,int y)// 意义很明确的函数名称
{
make_root(x);
if(find(y)!=x)t[x].ff=y;
return ;
}
void cut(int x,int y)// 意义很明确的函数名称
{
make_root(x);
if(find(y)==x&&t[y].ff==x&&t[y].ch[0]==0)
{
t[y].ff=t[x].ch[1]=0;
pushup(x);
}
return ;
}

这里再说一个细节:cut 的判断:

if(find(y)==x&&t[y].ff==x&&t[y].ch[0]==0)

第一个部分判的是 x,y 是否同属一个连通块,第二部分判的是 y 的父亲是否为 x.

而第三个判断就很抽象了,我们思考他的意义是什么:

首先,x 只有右子树。所以 dep[x]<dep[y]. 那么如果 y 有左子树:dep[x]<dep[yls]<dep[y] 对应原树的路径就是 x <- yls <- y 。这种情况下边 (x,y) 是不存在的。

再补充一下 splay 的部分:由于我们之前 make_root 时有一个翻转左右儿子的操作,这个操作显然是要 从上到下 进行 pushdown 的,所以我们在 splay 时要先 从上到下 进行 pushdown

st[++st[0]]=y;
while(isroot(y))st[++st[0]]=y=t[y].ff;//记录 x->root 上的每个点然后将它们从上到下 pushdown
while(st[0])pushdown(st[st[0]--]);//应为树有可能发生左右翻转(换根时),所以需要 pushdown 来维护其现在的真实形态

然后这题就做完了。

Code:

#include<bits/stdc++.h>
const int N=1e5+5;
using namespace std;
int st[N];
struct LCT{
struct Tree{
int sum,val,tag,ff,ch[2];
//首先明确,这颗平衡树的键值是该节点在原树中的深度
}t[N<<2];
#define ls t[x].ch[0]
#define rs t[x].ch[1]
#define fa t[x].ff
inline bool isroot(int x)
{
return (t[fa].ch[0]==x||t[fa].ch[1]==x);//这里的 isroot 返回的其实是他是否是 root 的反,所以其实是 isn'troot
}
inline void pushup(int x)
{
t[x].sum=t[ls].sum^t[rs].sum^t[x].val;
return;
}
inline void rev(int x)
{
swap(t[x].ch[0],t[x].ch[1]);
t[x].tag^=1;
return ;
}
inline void pushdown(int x)
{
if(t[x].tag)
{
if(ls)rev(ls);
if(rs)rev(rs);
t[x].tag=0;
}
return ;
}
inline void rotate(int x)
{
int y=fa,z=t[fa].ff,k=t[fa].ch[1]==x ? 1 : 0;
if(isroot(y))t[z].ch[t[z].ch[1]==y]=x;//isn't root
t[x].ff=z;
t[y].ch[k]=t[x].ch[!k];
if(t[x].ch[!k])t[t[x].ch[!k]].ff=y;
t[x].ch[!k]=y;
t[y].ff=x;
pushup(y);
}
inline void splay(int x)
{
int y=x,z=0;
st[++st[0]]=y;
while(isroot(y))st[++st[0]]=y=t[y].ff;//记录 x->root 上的每个点然后将它们从上到下 pushdown
while(st[0])pushdown(st[st[0]--]);//应为树有可能发生左右翻转(换根时),所以需要 pushdown 来维护其现在的真实形态
while(isroot(x))//isn't root
{
y=fa,z=t[fa].ff;
if(isroot(y)){rotate((t[y].ch[1]==x)==(t[z].ch[1]==y) ? y : x);}
rotate(x);
}
pushup(x);
}
void access(int x)
{
int y=0;
while(x)
{
//每次 splay 完,x 是没有左子树的,因为在目前合并出来的这颗 平衡树上,x 在原树中的深度最小。
//而它的右儿子又被我们强行赋为了 rt->x 这条链所形成的平衡树
//这样就保证了在 access 完了之后,这颗平衡树维护只存在 x->root 这段路径上的点
splay(x);rs=y;pushup(x);
y=x;x=fa;
}
}
void make_root(int x)//换根
{
access(x);splay(x);
rev(x);
}
int find(int x)//找到 x 所在的 平衡树/联通块的根 (动态的树,有可能是一个森林,所有节点的根并不统一)
{
access(x);splay(x);
while(ls)pushdown(x),x=ls;
splay(x);
return x;
}
void splite(int x,int y)//我也不知道这个函数为什么叫 splite 明明就是在把 x->y 这条路径找出来好吧
{
make_root(x);
access(y);splay(y);
}
void link(int x,int y)// 意义很明确的函数名称
{
make_root(x);
if(find(y)!=x)t[x].ff=y;
return ;
}
void cut(int x,int y)// 意义很明确的函数名称
{
make_root(x);
if(find(y)==x&&t[y].ff==x&&t[y].ch[0]==0)
{
t[y].ff=t[x].ch[1]=0;
pushup(x);
}
return ;
}
}T;
int n,m;
void work()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%d",&T.t[i].val);
}
for(int i=1,opt,x,y;i<=m;i++)
{
scanf("%d%d%d",&opt,&x,&y);
if(opt==0){T.splite(x,y);printf("%d\n",T.t[y].sum);}
if(opt==1){T.link(x,y);}
if(opt==2){T.cut(x,y);}
if(opt==3){T.splay(x);T.t[x].val=y;}
}
}
int main()
{
//freopen("LCT.in","r",stdin);freopen("LCT.out","w",stdout);
work();
return 0;
}
posted @   liuboom  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek-R1本地部署如何选择适合你的版本?看这里
· 开源的 DeepSeek-R1「GitHub 热点速览」
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 揭秘 Sdcb Chats 如何解析 DeepSeek-R1 思维链
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
点击右上角即可分享
微信分享提示