Link-Cut-Tree 动态树
2023-09-10 11:35:01
发布于:广东
PS:由于本菜坤能力有限,把很多实边写成了实链,大家就将就着看吧嘿嘿。
引入:仨链部分
我们一般提到的三种剖分是:重链剖分,实链剖分,和长链剖分。
前面我们已经讲过的树链剖分实际上就是重链剖分的别称。长链剖分不太常用(但是sslz_fsy非常强三个都会orz),而本次LCT用到的就是实链剖分。
实链部分
这个不需要特别学习,你只需要知道这种剖分的优点在于:可以调整,即可以变化。(不像重链剖分那样是固定的)
同样,每个节点对儿子只有一个实链,其它都是虚链。这样做的原因是方便splay来维护。(后话)
它的体现是:所有子节点都有father,但father的son只有实链链接的子节点或者原树上的父节点。意思是说你从叶子节点可以一直跳跳跳跳到根节点,每个叶子节点都可以,但对于一个根节点往下走,它的路径是唯一的。
接下来我们说一下,splay在树里面的运转方式。
splay
首先明确:每一个splay维护的是一个深度严格单调递增的链(简单说就是一个严格从祖先到后代的链)。用实链连接起来的一堆点在同一个splay里面。所以整个森林实际上也有很多个splay。
所以说实边虚边在LCT中的体现实际上就是在不在一个splay里面。
每个splay结点维护的值是其在splay中的整个子树的信息和。(比如取最大最小啦,求和或者异或啦)
因为维护的是信息和,所以在作修改的时候为了不影响到别的点,你要先splay上去。
这样就方便我们调用。至于为什么方便,我也说不清楚 (手动doge)。
基本函数学习
1.Access
access函数是非常基本的函数。非常重要。它的意义是:将该点到根节点的这个路径上的所有链变成实链。
这句话的潜含义有两点。
- 因为每个点只有一条向下的实链,所以实际上它是将一些实边转换成了轻边,然后硬生生的把一些虚边变成了实边。
- 因为splay维护实边连接起来的点,所以现在,根节点到该节点路径上的所有点包括它们二位都在一个splay里面了。
既然在一个splay里面了,那,就好办了噻嘿嘿。通过完美的pushup和pushdown,我们就能轻松调用里面的信息了。
相信我,代码比你们想的简单多了。
void access(int x){
for(int y;x;y=x,x=t[x].fa){//y永远是x下面的那个点
splay(x);//这个时候是没有右儿子的了。
t[x].ch[1]=y;//所以现在有了ovo
pushup(x);
}
}
2.Markeroot
makeroot的含义是,将这个点变成所在树的根,也就是换根。怎么办呢QAQ?
首先,为了让这位可怜的点点和十万八千里外的根节点联系上,我们进行的第一步是:access。
现在它们在一个splay里面啦!然后呢?然后我们会注意到这么一个性质。
由于splay按照深度严格递增维护,所以access完后的这个点是splay里面深度最深的点,换句话说
将这个点splay到根节点,它就不会有右儿子了(ovo)
于是我们splay了。emm然后?既然我们要换根,那如何在splay里面,把这个点变成深度最浅的那个根呢?
所以我们用上了大名鼎鼎的:翻转打标记。想想看,splay完后的这个splay实际上很左倾(doge),但翻转完后,就变成彻彻底底的右倾了(doge。
是的,总的来说,access,splay,打标记。
void makeroot(int x){
access(x);splay(x);push(x);//用来打标记的函数
}
3.Findroot
findroot顾名思义,找这个点所在树的根节点。
同样,为了和根节点擦出 爱的 火花,我们要先access一下。然后splay。
还是一个左倾树(乱编的名字),那我们就一直找左儿子,一直找下去就可以啦!记得下传标记哦!
顺带一提,为了防止pushup对splay上维护的值产生影响,我们要把这个点splay后才能pushup。
int findroot(int x){
access(x);splay(x);
while(t[x].ch[0]){
pushdown(x);
x=t[x].ch[0];
}splay(x);
pushup(x);
return x;
}
4.Link
链接操作,非常优秀。
对于两个点要链接,首先我们还是要makeroot其中一个点。然后要判重的话,就要判findroot另一个点的返回值是不是上一个点,是,说明有边不需要了,不是,说明要连边。
void link(int x,int y){
makeroot(x);
if(findroot(y)!=x)t[x].fa=y;//还是认父不认子
}
5.Cut
删边操作,也很优秀(万物皆油锈优秀)。
对于两个点要删边,首先我们还是要makeroot其中一个点。然后access,splay都是常规操作。然后如果要判是否有边的话,就要判断三个东西。
第一是findroot另一个点是不是上一个点,注意findroot完后,上一个点又又被splay成了根,覆盖了splay(y)。
第二个自然是这个点的父亲是不是上一个点。
第三个就是这个点不能有左儿子。
为什么呢?
因为findroot第二个点的时候把第一个点旋转成了splay 的根,则第二个点现在是第一个点的右儿子。而如果两者有边,说明二者深度必然是+1关系的,那在splay上,根的右儿子不能有左儿子。
void cut(int x,int y){
makeroot(x);access(y);splay(y);
if(findroot(y)==x&&t[y].fa==x&&!t[y].ch[0]){
t[y].fa=t[x].ch[1]=0;
pushup(x);//记得更新
}
}
6.Split
提取操作,非常优秀。
提取指定两点间的链,方便查询,思想跟树剖有点相似。具体而言就是makeroot,access,splay,查询。
int split(int x,int y){
makeroot(x);access(y);splay(y);
return t[y].pos;//因题而异
}
7.Splay变化了。
由于LCT的性质,导致翻转标记有时候可能在上面没有下传。所以splay之前,我们有一个下传标记的工作。
具体而言就是开一个栈,不断跳父亲把能装的都装进去,然后从上至下pushdown。
void splay(int x){
cnt=0;int y,z;
y=x;sta[++cnt]=y;
while(noroot(y)){
y=t[y].fa;sta[++cnt]=y;
}
while(cnt){
pushdown(sta[cnt--]);
}
while(noroot(x)){
if(noroot(y)){
(t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
}
rotate(x);
}
pushup(x);
}
同样与splay本身不同的是,此处我们判定旋转到根的意义就是,根的父亲的儿子不是根。(实边定义)(认父不认子)
所以noroot操作就是这样的:
int noroot(int x){
return t[t[x].fa].ch[0]==x||t[t[x].fa].ch[1]==x;
}
同时在rotate里面也要用noroot判断。
void rotate(int x){
int y=t[x].fa,z=t[y].fa,k=t[y].ch[1]==x,w=t[x].ch[k^1];
if(noroot(y)){
t[z].ch[t[z].ch[1]==y]=x;
}t[y].ch[k]=w;t[x].ch[k^1]=y;
if(w)t[w].fa=y;t[y].fa=x;t[x].fa=z;
pushup(y);pushup(x);
}
另外顺带一提,在LCT里面,我们建议pushdown的时候,每次都把自己孙子给push了。
也就是这样:
void push(int x){
swap(t[x].ch[0],t[x].ch[1]);t[x].tag^=1;
}
void pushdown(int x){
if(t[x].tag){
t[x].tag=0;
if(t[x].ch[0])push(t[x].ch[0]);
if(t[x].ch[1])push(t[x].ch[1]);
}
}
据说这样很严谨不会被卡?
pushup就是基本的操作就不写了。
例题可以参照某谷省选斗兽场的动态树。
放一个LCT某谷模板的代码。
#include<bits/stdc++.h>
#define in read()
#define il inline
#define lc t[x].ch[0]
#define rc t[x].ch[1]
using namespace std;
int in{
int cnt=0,f=1;char ch=0;
while(!isdigit(ch)){
ch=getchar();
if(ch=='-')f=-1;
}
while(isdigit(ch)){
cnt=cnt*10+ch-48;
ch=getchar();
}
return cnt*f;
}
int n,m;
int a[300003];
struct node{
int fa,ch[2],tag,val;
}t[300003];
int noroot(int x){
return t[t[x].fa].ch[0]==x||t[t[x].fa].ch[1]==x;
}
void pushup(int x){
t[x].val=t[lc].val^t[rc].val^a[x];
}
void push(int x){
swap(lc,rc);t[x].tag^=1;
}
void pushdown(int x){
if(t[x].tag){
if(lc)push(lc);
if(rc)push(rc);
t[x].tag=0;
}
}
void rotate(int x){
int y=t[x].fa,z=t[y].fa,k=t[y].ch[1]==x,w=t[x].ch[k^1];
if(noroot(y)){
t[z].ch[t[z].ch[1]==y]=x;
}
t[x].ch[k^1]=y;t[y].ch[k]=w;
if(w)t[w].fa=y;t[y].fa=x;t[x].fa=z;
pushup(y);pushup(x);
}
int sta[300003],cnt;
void splay(int x){
cnt=0;int y=x;sta[++cnt]=y;
while(noroot(y))sta[++cnt]=y=t[y].fa;
while(cnt){
pushdown(sta[cnt--]);
}
while(noroot(x)){
y=t[x].fa;int z=t[y].fa;
if(noroot(y)){
(t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
}rotate(x);
}
pushup(x);
}
int access(int x){
for(int y=0;x;y=x,x=t[x].fa){
splay(x);t[x].ch[1]=y;pushup(x);
}
}
int makeroot(int x){
access(x);splay(x);push(x);
}
int findroot(int x){
access(x);splay(x);
while(lc)pushdown(x),x=lc;
splay(x);
return x;
}
int link(int x,int y){
makeroot(x);
if(findroot(y)!=x)t[x].fa=y;
}
void split(int x,int y){
makeroot(x);access(y);splay(y);
}
int cut(int x,int y){
makeroot(x);
if(findroot(y)==x&&t[y].fa==x&&!t[y].ch[0]){
t[y].fa=t[x].ch[1]=0;
}
}
int main(){
n=in;m=in;
for(int i=1;i<=n;i++){
a[i]=in;
}
while(m--){
int opt=in,x=in,y=in;
switch(opt){
case 0:split(x,y);printf("%d\n",t[y].val);break;
case 1:link(x,y);break;
case 2:cut(x,y);break;
case 3:splay(x);a[x]=y;
}
}
return 0;
}
解释一下,对于3号操作,修改操作,为了防止修改完后对平衡树造成不当影响,我们先把它转到根,再修改。
全部评论 11
竟然打了5885个字符(包括空格),震惊我yee整年
2023-09-10 来自 广东
0两年半(改正
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
顶
2023-09-10 来自 广东
0顶
2023-09-10 来自 广东
0
有帮助,赞一个