LCT感性瞎扯
LCT。
I.LCT可以干什么?
动态树问题,
是近几年在OI中兴起的一种新型问题,
是一类要求维护一个有根树森林,
支持对树的分割,合并等操作的问题。
由Robert E Tarjan为首的科学家们
提出解决算法Link-Cut Tree,简称LCT。
——《百度百科》
算了,看看就行。
我们唯一知道的就是,你们的大毒瘤,那个发明强连通分量算法、HLPP、splay、离线LCA算法的Tarjan,他又跑来祸害咱们了!
附上高清大图
通俗点说,它支持你将一棵树一刀两断,也支持你把两棵树嫁接在一起(无性生殖?),还支持你从树上扯下来一条路径在上面搞事情。
或者换句话说,它就是(动态树剖+并查集+平衡树),再加上一堆奇妙的特性。
这么酷炫的吗!!!
好的那我们就开始吧。
II.前置芝士
splay:必修,特别是区间操作(fhq treap亦可)
树剖:选修
并查集:必修
线段树:必修
III.思想
我们常说的树链剖分,实际上是重链剖分。它还有两个兄弟,长链剖分与实链剖分。
这仨家伙有一个共同点:毒瘤可以把一棵树按照一些规则剁成几条不相交的路径。并且,这些路径上的点按照深度递增,不存在两个相同深度的点处在同一条路径上。
例如重链剖分,就是每个点和它所有子节点中最大的点在同一条链上。
而长链剖分,我没学过,咱也不敢瞎说
LCT运用的就是里面最灵活的实链剖分。我们常见的重链剖分,一旦剖分完成,是不可以再改变链的构成的,除非你暴力重构树。
但是,实链剖分,你就可以任意指定一个点的实边和实儿子。当然咯,这么方便也不是没有坏处,在极端情况下,实链剖分是的,不像重链剖分是严格的。
因此,我们就将实链剖分和灵活的splay结合在一起,得到了均摊的LCT。
我们将每一条实链,扔进一颗splay中维护。这棵splay以深度为键值,深度越大排名越靠后。
当然咯,因为splay的结构,我们没有必要建出一棵一棵的splay出来。它还是可以保有一个完整的结构的。只不过,对于某棵splay的树根,它有一个父亲,那是原树中它的父亲;但是它的父亲尽管有这个儿子,但它却不认!
例如这个剖分:
在以为根的那棵splay中,认了一个父亲,。但是,却不认这个儿子。它唯一承认的儿子,是,它splay中的右儿子。当然,也承认这个父亲。
但是,splay不一定只有一种构造。也有可能,是splay的根,而是它的左儿子。这个时候,和仍然互相承认,还是不承认,但是承认。
因此,我们可以看出,不管哪个儿子,都是承认与父亲的关系的。但是,父亲只承认与实儿子的关系(尽管这个实儿子有可能在splay中成为了它的父亲甚至有可能离他很远很远)。
因此我们便解锁了LCT中一个最重要的性质:实边认父也认子,虚边认父不认子。
IV.约定
:儿子,默认为的。为左儿子,为右儿子。
:父亲,默认为的。
:所在的splay的根。
:所在的原树的根(注意区分!!!)。
V.实现
效果:将节点到这条路径上的所有边设成实边(并自动把其他边设成虚边),并且扔到一个splay里面。
例:
怎么实现呢?
这个时候,我们就要回想起splay的经典操作:将一个旋转到它所在的的。
这个时候,我们就可以向转移了(因为是,它的父亲一定处在不同的splay中)。
因为的深度一定大于的深度,所以如果我们将也splay到它所属的splay的根,则可以直接设成的右儿子(在设的同时,原本的右儿子,原来是实儿子,被直接断成了虚儿子,认父不认子,成了家庭餐具了;反而,原来认子不认父的,现在重新承认了这个儿子,当然也一直承认着,因此这便成为了新的实边)。
看一下代码:
inline void access(int x){
for(register int y=0;x;x=t[y=x].fa)
splay(x),rson=y,pushup(x);
}
就是将转到根;将的右儿子设成;将变成新的(在的父亲时继续更新)。
至于这个,是更新的函数,类似于线段树的。
效果:将设为这棵树的(注意不是splay的)!
先上代码:
inline void makeroot(int x){
access(x),splay(x),REV(x);
}
,将这条路径上的点打包成一个splay;
,将设成这个的根。
但是,注意,因为在整棵中深度最大,它此时并没有右儿子!
因此,在调用函数后,整棵splay翻转过来,成为深度最浅的那一个!
因为这棵splay中深度最浅的是根节点,所以这个时候,就成为了新根。
效果:将到的路径打包成一个splay,找到的,并将转到。
先上代码:
inline int findroot(int x){
access(x),splay(x);
while(lson)pushdown(x),x=lson;
splay(x);
return x;
}
可以类比冰茶姬的找父亲操作。
函数打包,函数转到,那个循环找到深度最浅的点,即为,再就把转到(保证深度为级别),并返回。
如果你把这些函数的目的都能想清楚,就没有问题了。
效果:将到路径上的所有点打包成一个splay,并令为。
代码:
inline bool split(int x,int y){
if(findroot(x)!=findroot(y))return false;
makeroot(x),access(y),splay(y);
return true;
}
如果和不在同一颗树中,显然操作不合法,直接退出;
否则,把设成新,这样子再一下就抽出了的路径。由于就是,这就是到的路径。然后再将 到,保证深度。
效果:在节点间连一条边。
代码:
inline bool link(int x,int y){
makeroot(x);
if(findroot(y)==x)return false;
t[x].fa=y;
return true;
}
把转到;如果发现已经连通(在同一棵子树中),忽略之;否则,将的父亲设成,相当于连一条虚边。
效果:断掉节点间的边。
代码:
inline bool cut(int x,int y){
makeroot(x);
if(findroot(y)!=x||t[y].fa!=x||t[y].ch[0])return false;
t[x].ch[1]=t[y].fa=0;
pushup(x);
return true;
}
这是仅次于最重点的一个,也是仅次于最难的那个。
先让为;如果,即它们不在同一棵树中,忽略之;如果,则显然它们之间不可能有边,因为后,和处于同一棵splay中,且是根(不明白的马上转回去看),则深度差为的点对必然紧贴;就算这两点都满足,如果有左儿子,则和仍然没有真正地紧贴,它们之间也不可能有边;这些都是需要忽略的。
之后,就是断边了。因为这时为根,且的深度小于,因此的右儿子一定就是。
VI.汇总
struct LCT{
int fa,ch[2],val,sum;
bool rev;
}t[100100];
inline int identify(int x){
if(x==t[t[x].fa].ch[0])return 0;
if(x==t[t[x].fa].ch[1])return 1;
return -1;
}
inline void pushup(int x){
t[x].sum=t[lson].sum^t[rson].sum^t[x].val;
}
inline void REV(int x){
t[x].rev^=1,swap(t[x].ch[0],t[x].ch[1]);
}
inline void pushdown(int x){
if(!t[x].rev)return;
if(lson)REV(lson);
if(rson)REV(rson);
t[x].rev=0;
}
inline void rotate(int x){
register int y=t[x].fa;
register int z=t[y].fa;
register int dirx=identify(x);
register int diry=identify(y);
register int b=t[x].ch[!dirx];
if(diry!=-1)t[z].ch[diry]=x;t[x].fa=z;
if(b)t[b].fa=y;t[y].ch[dirx]=b;
t[y].fa=x,t[x].ch[!dirx]=y;
pushup(y),pushup(x);
}
inline void pushall(int x){//pushdown the nodes in the route from x to root
if(identify(x)!=-1)pushall(t[x].fa);
pushdown(x);
}
inline void splay(int x){//splay x to the root
pushall(x);
while(identify(x)!=-1){
register int fa=t[x].fa;
if(identify(fa)==-1)rotate(x);
else if(identify(x)==identify(fa))rotate(fa),rotate(x);
else rotate(x),rotate(x);
}
}
inline void access(int x){//pull out all the nodes in the route from x to the ROOT, and form a splay
for(register int y=0;x;x=t[y=x].fa)splay(x),rson=y,pushup(x);
}
inline void makeroot(int x){//make x the new ROOT
access(x),splay(x),REV(x);
}
inline int findroot(int x){//find the ROOT of x, and make ROOT the root
access(x),splay(x);
while(lson)pushdown(x),x=lson;
splay(x);
return x;
}
inline bool split(int x,int y){//pull out the route from x to y and form a splay rooted y; if there isn't such route,return false
if(findroot(x)!=findroot(y))return false;
makeroot(x),access(y),splay(y);
return true;
}
inline bool link(int x,int y){//link an edge between x and y; if they have already connected before, return false.
makeroot(x);
if(findroot(y)==x)return false;
t[x].fa=y;
return true;
}
inline bool cut(int x,int y){//cut the edge between x and y; if there isn't such edge, return false
makeroot(x);
if(findroot(y)!=x||t[y].fa!=x||t[y].ch[0])return false;
t[x].ch[1]=t[y].fa=0;
pushup(x);
return true;
}
注意到几处与普通splay的不同:
1.在函数判断左儿子还是右儿子时,如果,则说明是。
2.在时调用函数,从上到下递归地。
3.在时,在多个可能为或者该节点不存在的地方进行特判。
I.【模板】Link Cut Tree (动态树)
直接把那份模板加个头尾就能过。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?