这一个月貌似已经考了无数次\(LCT\)了.....
保险起见还是来一发总结吧.....
A. LCT 模板
\(LCT\) 是由大名鼎鼎的 \(Tarjan\) 老爷发明的。
主要是用来维护树上路径问题的。 它的神奇之处在于可以直接把一条路径抠出来维护。
其实就是维护树链剖分中的重链与轻链。
网上相关教程很多,直接给板子(其实是我懒得打E_E)
0. Splay代码:
\(LCT\)是以\(Splay\) 为实现基础的:
IL bool Son(RG int x){return ch[fa[x]][1] == x;}
IL bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }
IL void Rot(RG int x){
RG int y = fa[x] , z = fa[y] , c = Son(x);
if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;
ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
ch[x][!c] = y; fa[y] = x; PushUp(y);
}
IL void Splay(RG int x){
RG int top = 0; stk[++top] = x;
for(RG int i = x; fa[i]; i = fa[i])stk[++top] = fa[i];
while(top)PushDown(stk[top--]);
for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])
if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
PushUp(x); return;
}
1. Access(x) 操作
作用是把 \(x\) 到根节点的这条路径变为重链。
void Access(int x){
for(RG int y=0;x;y=x,x=fa[x])
Splay(x) , ch[x][1] = y , PushUp(x);
}
}
2. MakeRoot(x) 操作
作用是使 \(x\) 节点成为原树中的根。
IL void Makeroot(RG int x){Access(x); Splay(x); Reverse(x);}
对应的辅助函数:
IL bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}
IL void PushDown(RG int x){
if(rev[x])
rev[x] ^= 1 ,
Reverse(ch[x][0]) , Reverse(ch[x][1]) ;
}
3. FindRoot(x) 操作
作用是找到\(x\)所在原树的根节点。
IL int FindRoot(RG int x){
Access(x); Splay(x);
while(ch[x][0])x=ch[x][0]; return x;
}
4. Split(x,y) 操作
作用是分离出\(x\)到\(y\)这条路径。
IL void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}
那么此时路径信息都存在节点 \(y\) 上了,要查询什么直接查即可。
5. Link(x,y) 操作
作用是连接两个结点\(x\) , \(y\)
IL void Link(RG int x,RG int y){MakeRoot(x); fa[x] = y; }
6. Cut(x,y) 操作
作用是删除\(x\) , \(y\) 之间的连边
IL void Cut(RG int x,RG int y){Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}
7. PushUp(x) ?PushDown(x) 操作
其实是与线段树一样的,直接维护题目所求即可。
代码略,以实际题目为准(注意翻转左右儿子的\(PushDown\)是必须有的)。
8.贴一发完整的\(LCT\)模版
namespace Link_Cut_Tree{
bool Son(RG int x){return ch[fa[x]][1] == x;}
bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }
bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}
void PushUp(RG int x){ ....... }
void PushDown(RG int x)
{if(rev[x])Reverse(ch[x][0]) , Reverse(ch[x][1]) , rev[x] ^= 1;}
void Rot(RG int x){
RG int y = fa[x] , z = fa[y] , c = Son(x);
if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;
ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
ch[x][!c] = y; fa[y] = x; PushUp(y);
}
void Splay(RG int x){
RG int top = 0; stk[++top] = x;
for(RG int i = x; fa[i]; i = fa[i])stk[++top] = fa[i];
while(top)PushDown(stk[top--]);
for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])
if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
PushUp(x); return;
}
void Access(RG int x)
{ for(RG int y = 0; x; y = x,x = fa[x])Splay(x),ch[x][1] = y,PushUp(x); }
void MakeRoot(RG int x){Access(x); Splay(x); Reverse(x);}
int FindRoot(RG int x){Access(x); Splay(x); while(ch[x][0])x=ch[x][0]; return x;}
void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}
void Link(RG int x,RG int y){MakeRoot(x); fa[x] = y; }
void Cut(RG int x,RG int y){Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}
}
B. LCT 维护 边权 信息
具体的实现方法有几种,这里只讲一种最简单、最常用的方法。
对于每一条边,新建一个点,然后连向对应的两个原树节点。
那么常见的维护方法是 代表点的节点不赋值,只有代表边的节点才赋值。
具体可以见这一道非常经典的题目: [NOI2014]魔法森林
伪代码为:
\(Link\)操作直接进行即可。
IL void Link(Node u,Node v)
NewNode(val_edge) ---(give)---> code_edge
Link(u,code_edge); Link(v,code_edge);
\(Cut\)操作则需要找到这条边对应的两个节点,然后直接删除即可。
IL void Cut(Edge e)
Edge e ---(find)--> Node u1,u2 ;
Delete(u1,code_e); Delete(u2,code_e);
这里注意 :
一定要找到对应的节点,而不是code_e的左右儿子!! ,不然保准你 WA 的怀疑人生。
然后一个非常经典的应用 就是 动态求图的割边(桥):
具体见这题:[AHOI2005]LANE 航线规划 ;;;;;;;;
C. LCT 维护 子树 信息
理论上来说\(LCT\)是不适合维护子树信息的,但总有一些毒瘤的出题人**..
给一篇写的非常详细的博客:http://blog.csdn.net/neither_nor/article/details/52979425
怎么搞呢?
先明确一下概念。
\(LCT\) 链剖后,一些路径变为了 单独的 一棵\(Splay\) ,
我们类似树链剖分,把节点划为实儿子、虚儿子。
实儿子就是在同一棵\(Splay\)里的那个儿子,其它则为虚儿子。
那么实儿子对应的子树就为实子树,虚儿子对应的子树就为虚子树。
.
其实我们原来的\(LCT\)就是维护了实子树对吧?
所以我们在维护子树信息时,只要再维护一下虚子树的信息即可。
以求子树大小为例。
我们假设\(sz[u]\)表示\(u\)的虚子树大小 , \(sum[u]\)表示整个子树大小,
那么显然,我们\(PushUp\)时,只需要更新\(sum\) , \(sz\)我们手动维护。
IL void PushUp(RG int x){ sum[x] = sum[ch[x][0]]+sum[ch[x][1]]+sz[x]+1; }
那么考虑什么情况下会改变虚子树的大小,其实只有两个:
1. Access‘(x) 操作
我们会把 \(x\) 原来的实儿子变为虚儿子,把另一个虚儿子变为实儿子。
我们对应的修改一下\(x\)的\(sz\)值,然后\(PushUp\)修改\(sum\)值即可。
IL void Access(RG int x){
for(RG int y = 0; x; y = x,x = fa[x]){
Splay(x);
sz[x] += sum[ch[x][1]] - sum[y];
ch[x][1] = y; PushUp(x);
}return;
}
2. Link‘(x,y) 操作
我们把\(x\)变为\(y\)的儿子。
那么显然\(x\)以及其所有祖先的\(sz\)值都会收到影响。
解决办法非常简单,把\(y\)也 \(MakeRoot\) 一下即可使得\(x\)只影响 \(y\) 。
注意一下由于我们修改了\(y\)的\(sz\)值,所以要\(PushUp\)一下\(y\)节点。
IL void Link(RG int x,RG int y){
MakeRoot(x); MakeRoot(y);
fa[x] = y; sz[y] += sum[x]; PushUp(y);
}
3.应用
应用一般就是求解会不断换根的子树信息问题。
最经典的一道题目是:「BJOI2014」大融合