LCT总结(个人笔记)

柒葉灬

2019-03-10 14:46:24

Personal

# LCT总结 -------- >前置技能:$splay$ ### 介绍: LCT即 Link-Cut-Tree, 其中Link指连接,Cut指断开,Tree则是一棵树。 说白了就是一棵**动态树**, 可以**动态删除边,添加边。** 擅长处理**链上的问题**, 但是**子树问题**也可以解决,后面会有例题。 ----- 当然,作为一个优秀的数据结构$LCT$, 肯定不能像普通的树一样暴力跳父亲, 肯定要有优秀的方法快速找根。 但普通的树是用 **倍增** 和 **重链剖分**。 >_(或许还有**长链剖分**_ 而这两种算法都不能修改。 所以我们用的是另一种方法:**实链剖分** ----- ## 实链剖分 ------- 这里先介绍一下什么是 **实链剖分** 。 实际上就是选一个儿子与自己的边当做**实边**, 其他边都是**虚边** >实边需要满足什么条件? >实边不需要任何条件, >就是自己随便选的, >就是因为是随便选的,所以才可以**动态连边删边**。 这些实边就形成了实链。 _当然也可以都是虚边_ ------- 我之前在一篇博客里面看到一句话, 很好的概括了实边和虚边的区别: >认子不认父 即每个点**不用管自己和父亲连的是实边还是虚边**, 只要记录**与哪一个儿子连实边**就行了。 如果需要判断自己和父亲是不是连实边 只需要判一下**父亲的实儿子**是不是自己就行了 >感觉有什么怪怪的??? ----- 那么有了实链之后有什么用? 怎么把实链用在$LCT$上? >想一下如果是重链剖分的话,应该怎么找根呢? 我们可以一直跳到实链的顶端,直到到达根为止。 但是这样还有几个问题需要解决: 1. 怎么保证跳的实链的次数不会过大? 2. 怎么跳到每条链的顶端的父亲? ----- #### 第一个问题:怎么保证跳的实链的次数不会过大? 我们可以每次把当前的点和根变成一条实链, 以此来保证复杂度的合理性。(感性理解) ### 第二个问题:怎么跳到每条链的顶端的父亲? 重链剖分里面是直接记录每一个点的$top$指向哪里。 而实链剖分就不能这么做,因为$top$随时可能会变。 这时候就要用到最核心的知识了, **把每一条实链用一个splay来表示**: 1. **左儿子**深度都比当前点**浅** 2. **右儿子**深度都比当前点**深** splay当中最左边的数就是链的顶端了。 但,链顶的点不一定是splay的根怎么办? 即,可能链顶的点的$fa$表示的是**splay**当中的$fa$ 我们注意到,$splay$当中,根是没有父亲的。 所以我们可以这么做, 每个$splay$的根的$fa$表示**链顶的点在树中的父亲** ------ 做个图以便理解: ![](https://cdn.luogu.com.cn/upload/pic/53686.png ) _其中加粗的是实边,其他的是虚边。_ 而实际上每一个实链是用**splay**来表示的, 所以可能实际上$son$和$fa$的关系是这样的。 ![](https://cdn.luogu.com.cn/upload/pic/53687.png ) 其中每一个框就是一个$splay$ ------ >?**mmp**为什么写了半天的概念 接下来就是怎么设计代码了。 $up,down$ 以及 $pushr$(翻转)与普通$splay$一样。 多了一个判断是不是$splay$的根的函数$noroot$ ```cpp bool noroot(int x){ return son[fa[x]][0]==x||son[fa[x]][1]==x; }//如果不是splay的根返回true; ``` 首先,LCT当中的$splay$的$rotate$和$splay$ 是不能复制普通$splay$的, **有些不同。** ### rotate操作 ```cpp void rotate(int x){ int y=fa[x]; int z=fa[y]; int k=(son[y][1]==x); if(noroot(y))son[z][(son[z][1]==y)]=x; fa[x]=z; son[y][k]=son[x][k^1]; fa[son[x][k^1]]=y; son[x][k^1]=y; fa[y]=x; up(y);up(x); } ``` 为什么需要判断$noroot(y)$呢? 因为如果$y$是根则说明$z$不在$x$这个$splay$内 如果不加则会改变$z$的**实儿子**,故错。 ------ ### splay操作 ```cpp void splay(int x){ int u=x,top=0; stk[++top]=u; while(noroot(u))stk[++top]=u=fa[u]; while(top)down(stk[top--]); //即先down完之后再splay while(noroot(x)){ int y=fa[x]; int z=fa[y]; if(noroot(y)) (son[z][1]==y)^(son[y][1]==x)?rotate(x):rotate(y); rotate(x); } } ``` ---- 以上都是处理实链的操作,那么怎么造出实链呢? 下面这个操作就是$LCT$的核心操作了。 ### access操作 ```cpp void access(int x){ for(int y=0;x;y=x,x=fa[x]) splay(x),son[x][1]=y,up(x); } ``` 这段代码什么意思呢? 即造出$x$到$root$这条实链,之前的其他实边变为虚边。 循环里面是把$x$与$y$连上实边,之前的实边被覆盖。 1. $splay(x)$:把x旋转到根 2. $son[x][1]=y$:之前的实边被覆盖,实儿子变成y。 注意链是从$root$到$x$的, 所以$x$实际上位于**实链的最底端,$splay$的最右端。** ------- 接下来的代码想想就都能明白了 ### makeroot操作 ```cpp void makeroot(int x){ access(x);splay(x);pushr(x); } ``` 把$x$变成树的根。 ------- ### findroot操作 ```cpp int findroot(int x){ access(x);splay(x); while(son[x][0])down(x),x=son[x][0]; splay(x);//保证复杂度 return x; } ``` 还有一个操作可以把要修改的链给拎出来 ### split操作 ```cpp void split(int x,int y){ makeroot(x);access(y);splay(y); //直接对y节点进行询问即可 } ``` 最后就是连边和删边操作了 ### link操作 所连的边可能已经存在了,那么这么写 ```cpp bool link(int x,int y){ makeroot(x); if(findroot(y)==x)return false; fa[x]=y;//直接连接虚边 return true; } ``` 如果数据中保证边合法,那么可以这么写 ```cpp void link(int x,int y){ makeroot(x); fa[x]=y; } ``` ----- ### cut操作 所删的边可能不存在,需要这么写 ```cpp bool cut(int x,int y){ makeroot(x); if(findroot(y)!=x||fa[y]!=x||son[y][0])return false; son[x][1]=fa[y]=0; up(x); return true; } ``` 如果数据中保证边合法,那么可以这么写 ```cpp void cut(int x,int y){ split(x,y); son[y][0]=fa[x]=0; up(y); } ``` ------- 那么$LCT$基本就已经讲完了,接下来就是做题经验了。 ### LCT在基环树当中的应用 基环树当中有一个环,但显然树里面不能有环, 所以可以用$LCT$把基环树变成树。 即把环中的一个点变成根, 再把连成环的那条边单独记录下来, 如果环被破坏掉了则连回去。 [LCT专题L](https://cn.vjudge.net/contest/282972#problem/L) ### LCT维护子树信息 $LCT$实际上也可以维护子树的信息 只不过略微麻烦一点, 就是多维护一个虚儿子的信息, 在$access$的时候注意修改就行了。 [LCT专题K](https://cn.vjudge.net/contest/282972#problem/K) ------ END