前言

\(LCT\),真的是一个无比神奇的数据结构。

它可以动态维护链信息连通性边权子树信息等各种神奇的东西。

而且,它其实并不难理解。

就算理解不了,它简短的代码也很好背。

\(LCT\)与实边的定义

\(LCT\),全称\(Link\ Cut\ Tree\),中文名动态树

它的实现有点类似于树链剖分,但树链剖分维护的是重边轻边(故又称重链剖分),而\(LCT\)维护的则是实边虚边

什么是实边?

我们选择一个节点与其一个儿子的连边为实边,与其他儿子的连边为虚边,这里的实边是可以随时变化的。

而实链剖分与树链剖分最大的区别在于,树链剖分是静态的,所以可以用线段树维护,而实链剖分则是动态的,因此就需要用一个更为神奇的数据结构——\(Splay\)来进行维护。

于是,就有了\(LCT\)这个奥秘重重的数据结构。

\(LCT\)的简单性质

从上面的内容我们可以知道,\(LCT\)将一棵树的边分成了实边虚边

而连续的若干条实边构成了实链

而我们对每条实链分别对每个\(Splay\)进行维护,可以保证,每个\(Splay\)中维护的节点按中序遍历得到的顺序在原树中深度依次增加\(1\)(证明:因为我们维护的是一条连续的链啊)。

而虚边的作用则是将这些\(Splay\)给链接起来,大体连接方式如下:

  • 找到该\(Splay\)中在原树中深度最小的节点,记其为\(k\)。(具体代码实现时是无需求出这个\(k\)的,这里只是方便理解)
  • 如果\(k\)是原树中的根节点,则无需连边。
  • 否则,我们找到\(fa_k\),将该\(Splay\)的根节点与\(fa_k\)之间连一条边。

这样一来,就把所有\(Splay\)连在了一起。

注意到一个节点可能有多个儿子,但实际上它只存储一个儿子,某大佬用一句很精辟的话对其进行了总结:认父不认子

\(LCT\)的基本操作

下面,我们来介绍几个\(LCT\)的基本操作。

  • \(IsRoot(x)\)

    \(IsRoot(x)\)的作用是判断一个节点\(x\)是否是当前实树的根。

    由于我们知道\(LCT\)是认父不认子的,所以只需要判断当前节点的父亲节点的两个子节点是否为当前节点即可。

    代码如下:

inline bool IsRoot(int x)//判断一个节点x是否是当前实树的根
{
return node[node[x].Father].Son[0]^x&&node[node[x].Father].Son[1]^x;//判断fa[x]的两个子节点是否为x
}
  • \(Rotate(x)\&\&Splay(x)\)

    关于这个可以自行参考简析平衡树(三)——浅谈Splay

    然而\(Splay\)和\(LCT\)中这两个操作其实还是有一定区别的。

    比如说,\(LCT\)每次固定将节点旋到根,因此只需要一个参数(虽然我博客中\(Splay\)的第一个模板也是只传一个参的)。

    再比如,\(LCT\)在\(Splay\)前需要先将当前节点到根节点的路径上所有节点从上往下\(PushDown()\)一遍。这可以函数递归,也可以直接栈模拟。

    具体代码如下:

#define Which(x) (node[node[x].Father].Son[1]==x)
#define Connect(x,y,d) (node[node[x].Father=y].Son[d]=x)
inline void Rotate(int x)
{
register int fa=node[x].Father,pa=node[fa].Father,d=Which(x);
!IsRoot(fa)&&(node[pa].Son[Which(fa)]=x),node[x].Father=pa,Connect(node[x].Son[d^1],fa,d),Connect(fa,x,d^1),PushUp(fa),PushUp(x);
}
inline void Splay(int x)
{
register int fa=x,Top=0;
while(Stack[++Top]=fa,!IsRoot(fa)) fa=node[fa].Father;//存入栈中
while(Top) PushDown(Stack[Top]),--Top;//依次PushDown
while(!IsRoot(x)) fa=node[x].Father,!IsRoot(fa)&&(Rotate(Which(x)^Which(fa)?x:fa),0),Rotate(x);
}
  • \(Access(x)\)

    \(Access(x)\)的作用是把根节点到\(x\)的路径上的边全部变为实边

    则我们首先考虑在当前\(Splay\)中将\(x\)旋到根,然后将\(x\)与\(fa_x\)间的连边更新为实边,即更新\(fa_x\)的右儿子为\(x\);再将\(fa_x\)在其所在\(Splay\)中旋到根,同理更新\(fa_{fa_x}\)的右儿子为\(fa_x\)... ...

    以此类推,直到处理到根节点所在的\(Splay\)为止。

    这样就打通了一条从根节点到\(x\)的路径。

    \(Access(x)\)可谓是\(LCT\)最核心的操作,也是后面许多操作的基础。

    具体实现可以详见代码:

inline void Access(int x)//把根节点到x的路径上的边全部变为实边
{
for(register int son=0;x;x=node[son=x].Father)
Splay(x),node[x].Son[1]=son,PushUp(x);//注意Access过程中要PushUp
}
  • \(FindRoot(x)\)

    \(FindRoot(x)\)的作用是找到\(x\)所在的原树中的根节点,可以用来判断连通性,实现可撤销并查集

    我们首先\(Access(x)\)打通一条从根到\(x\)的路径,此时\(x\)就与根节点在同一个\(Splay\)内了。

    然后\(Splay(x)\)将\(x\)旋到根。

    记住前面提到的\(LCT\)的性质:每个\(Splay\)中维护的节点按中序遍历得到的顺序在原树中深度依次增加\(1\)

    所以根节点必然是\(Splay\)中中序遍历顺序为\(1\)的节点

    而这其实就是\(x\)尽量向左儿子拓展最后得到的节点。

    代码如下:

inline int FindRoot(int x)
{
Access(x),Splay(x);//一波操作,将x转到根节点所在Splay的根
while(node[x].Son[0]) PushDown(x),x=node[x].Son[0];//尽量向左儿子拓展,注意每次拓展前先PushDown
return Splay(x),x;//最后不忘Splay的优良传统:每执行完一个操作就Splay一下,防被卡
}
  • \(MakeRoot(x)\)

    \(MakeRoot(x)\)的作用是将\(x\)作为原树中的新的根节点

    首先,依然是先\(Access(x)\)打通一条从根到\(x\)的路径,然后\(Splay(x)\)将\(x\)旋到根。

    由前面的操作可知,根节点是\(Splay\)中中序遍历顺序为\(1\)的节点

    而此时,\(x\)必然是\(Splay\)中中序遍历最后得到的点。

    因此我们只要翻转该\(Splay\),\(x\)就变成中序遍历顺序为\(1\)的节点了。

    代码如下:

inline void Rever(int x)//翻转子树
{
swap(node[x].Son[0],node[x].Son[1]),node[x].Rev^=1;//交换左右儿子,然后更新标记
}
inline void MakeRoot(int x)//将x作为原树中的新的根节点
{
Access(x),Splay(x),Rever(x);//将x转到根节点所在Splay的根,然后翻转Splay
}
  • \(Link(x,y)\)

    \(Link(x,y)\)的作用是在\(x\)和\(y\)两个节点间连一条边

    首先,我们将\(x\)作为它所在树的根,即\(MakeRoot(x)\)。

    然后,我们需要判断\(x\)与\(y\)是否联通。由于\(x\)是其所在子树的根节点,因此只要判断\(FindRoot(y)\)是否为\(x\)即可。

    连接只需要更新\(x\)的父亲为\(y\)即可。

    代码如下:

inline void Link(int x,int y)//在x和y两个节点间连一条边
{
MakeRoot(x),FindRoot(y)^x&&(node[x].Father=y);//判断x和y的连通性,然后连接
}
  • \(Cut(x,y)\)

    \(Cut(x,y)\)的作用是删除\(x\)和\(y\)之间的边

    首先,我们依然将\(x\)作为它所在树的根。

    则可以保证,若\(x\)和\(y\)有边相连,一定满足一下三个条件:

    1. \(y\)所在树的根为\(x\),即\(FindRoot(y)==x\)。
    2. \(y\)的父亲节点为\(x\),即\(fa_y==x\)。
    3. \(y\)没有左儿子。因为如果\(y\)有左儿子,由于\(LCT\)的性质,可得\(Depth_x<Depth_{leftson_y}<Depth_y\),则\(x\)和\(y\)必然不相连。

    代码如下:

inline void Cut(int x,int y)//删除x和y之间的边
{
MakeRoot(x),!(FindRoot(y)^x)&&!(node[y].Father^x)&&!node[y].Son[0]&&(node[y].Father=node[x].Son[1]=0,PushUp(x));//判断x和y的连通性,然后删边
}
  • \(Split(x,y)\)

    \(Split(x,y)\)的作用是从\(LCT\)中抠出\(x\)与\(y\)之间的路径

    这样一来,就方便我们查询了。

    这个操作第一步便是将\(x\)作为根,然后打通\(x\)到\(y\)的路径。

    可以保证,此时\(x\)与\(y\)所在的\(Splay\)内只包含\(x\)与\(y\)路径上的节点

    然后我们将\(Splay(y)\)将\(y\)旋至\(Splay\)的根,这样一来就可以通过查询\(y\)的信息来进行询问了。

    代码如下:

inline void Split(int x,int y)//从LCT中抠出x与y之间的路径
{
MakeRoot(x),Access(y),Splay(y);//将x作为根,打通x与y的路径并将y旋到根
}

模板(板子题

#include<bits/stdc++.h>
#define N 300000
#define swap(x,y) (x^=y^=x^=y)
using namespace std;
int n,a[N+5];
class Class_FIO
{
private:
#define Fsize 100000
#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,Fsize,stdin),A==B)?EOF:*A++)
#define pc(ch) (FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,Fsize,stdout),Fout[(FoutSize=0)++]=ch))
int Top,FoutSize;char ch,*A,*B,Fin[Fsize],Fout[Fsize],Stack[Fsize];
public:
Class_FIO() {A=B=Fin;}
inline void read(int &x) {x=0;while(!isdigit(ch=tc()));while(x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));}
inline void writeln(int x) {while(Stack[++Top]=x%10+48,x/=10);while(Top) pc(Stack[Top--]);pc('\n');}
inline void clear() {fwrite(Fout,1,FoutSize,stdout),FoutSize=0;}
}F;
class Class_LCT
{
private:
#define LCT_SIZE N
#define PushUp(x) (node[x].Sum=node[x].Val^node[node[x].Son[0]].Sum^node[node[x].Son[1]].Sum)
#define Rever(x) (swap(node[x].Son[0],node[x].Son[1]),node[x].Rev^=1)
#define PushDown(x) (node[x].Rev&&(Rever(node[x].Son[0]),Rever(node[x].Son[1]),node[x].Rev=0))
#define Which(x) (node[node[x].Father].Son[1]==x)
#define Connect(x,y,d) (node[node[x].Father=y].Son[d]=x)
#define IsRoot(x) (node[node[x].Father].Son[0]^x&&node[node[x].Father].Son[1]^x)
#define MakeRoot(x) (Access(x),Splay(x),Rever(x))
#define Split(x,y) (MakeRoot(x),Access(y),Splay(y))
int Stack[LCT_SIZE+5];
struct Tree
{
int Val,Sum,Father,Rev,Son[2];
}node[LCT_SIZE+5];
inline void Rotate(int x)
{
register int fa=node[x].Father,pa=node[fa].Father,d=Which(x);
!IsRoot(fa)&&(node[pa].Son[Which(fa)]=x),node[x].Father=pa,Connect(node[x].Son[d^1],fa,d),Connect(fa,x,d^1),PushUp(fa),PushUp(x);
}
inline void Splay(int x)
{
register int fa=x,Top=0;
while(Stack[++Top]=fa,!IsRoot(fa)) fa=node[fa].Father;
while(Top) PushDown(Stack[Top]),--Top;
while(!IsRoot(x)) fa=node[x].Father,!IsRoot(fa)&&(Rotate(Which(x)^Which(fa)?x:fa),0),Rotate(x);
}
inline void Access(int x) {for(register int son=0;x;x=node[son=x].Father) Splay(x),node[x].Son[1]=son,PushUp(x);}
inline int FindRoot(int x) {Access(x),Splay(x);while(node[x].Son[0]) PushDown(x),x=node[x].Son[0];return Splay(x),x;}
public:
inline void Init(int len,int *data) {for(register int i=1;i<=len;++i) node[i].Val=data[i];}
inline void Link(int x,int y) {MakeRoot(x),FindRoot(y)^x&&(node[x].Father=y);}
inline void Cut(int x,int y) {MakeRoot(x),!(FindRoot(y)^x)&&!(node[y].Father^x)&&!node[y].Son[0]&&(node[y].Father=node[x].Son[1]=0,PushUp(x));}
inline void Update(int x,int v) {Splay(x),node[x].Val=v;}
inline int Query(int x,int y) {return Split(x,y),node[y].Sum;}
}LCT;
int main()
{
register int query_tot,i,op,x,y;
for(F.read(n),F.read(query_tot),i=1;i<=n;++i) F.read(a[i]);
for(LCT.Init(n,a);query_tot;--query_tot)
{
F.read(op),F.read(x),F.read(y);
switch(op)
{
case 0:F.writeln(LCT.Query(x,y));break;
case 1:LCT.Link(x,y);break;
case 2:LCT.Cut(x,y);break;
case 3:LCT.Update(x,y);break;
}
}
return F.clear(),0;
}

后记

推荐一些比较好的\(LCT\)题目:【转载】LCT题单

LCT入门的更多相关文章

  1. LCT入门总结

    原文链接https://www.cnblogs.com/zhouzhendong/p/LCT.html 为什么要写这个总结? 因为之前的总结出问题了…… 下载链接: LCT 入门总结 UPD(2019 ...

  2. LCT 入门

    这是一份 \(\rm LCT\) 入门总结. 关于 \(\rm LCT\) 的复杂度这里不会提及,只会记录 \(\rm LCT\) 的基本操作和经典例题,但神奇的 \(\rm LCT\) 虽然常数巨大 ...

  3. 「专题总结」LCT入门

    上次xuefeng说我的专题总结(初探插头dp)太不适合入门了,所以这次丢一些题解包以外的东西. 关键是我自己也不会...急需梳理一下思路... (让我口胡数据结构???顺便推广一下全世界最短的lct ...

  4. BZOJ2843:极地旅行社(LCT入门题)

    不久之前,Mirko建立了一个旅行社,名叫“极地之梦”.这家旅行社在北极附近购买了N座冰岛,并且提供观光服 务.当地最受欢迎的当然是帝企鹅了,这些小家伙经常成群结队的游走在各个冰岛之间.Mirko的旅 ...

  5. BZOJ2049:Cave 洞穴勘测 (LCT入门)

    辉辉热衷于洞穴勘测.某天,他按照地图来到了一片被标记为JSZX的洞穴群地区.经过初步勘测,辉辉发现这片区域由n个洞穴(分别编号为1到n)以及若干通道组成,并且每条通道连接了恰好两个洞穴.假如两个洞穴可 ...

  6. 【BZOJ4025】二分图(LCT动态维护图连通性)

    点此看题面 大致题意: 给你一张图以及每条边的出现时间和消失时间,让你求每个时间段这张图是否是二分图. 二分图性质 二分图有一个比较简单的性质,即二分图中不存在奇环. 于是题目就变成了:让你求每个时间 ...

  7. 【转载】LCT题单

    本篇博客的题单转载自FlashHu大佬的博客:LCT总结--应用篇(附题单)(LCT). 关于\(LCT\)可以查看这篇博客:\(LCT\)入门. 这里面有些题解的链接是空链接,尚未补全. 维护链信息 ...

  8. 【SDOI2008】解题汇总

    好叭我真的是闲的了... /---------------------------------------------/ BZOJ-2037 [SDOI2008]Sue的小球 DP+相关费用提前计算 ...

  9. bzoj2049: [Sdoi2008]Cave 洞穴勘测

    lct入门题? 得换根了吧TAT 这大概不是很成熟的版本.. #include<iostream> #include<cstring> #include<cstdlib& ...

随机推荐

  1. 读书印记 - 《文革前的邓小平:毛XX的副帅》

    开始看才发现这居然是本学术著作,阅读难度系数比小说.传记要很多,相比于小说的人物心理.传记的故事套路,这本书的基本写法是举一大坨材料来描述当时的事实然后稍微发表一点学术观点.....我对这个内容本身挺 ...

  2. poj 1001 字符串乘法以及处理精度问题

    #include<iostream> #include<cstring> using namespace std; int main() { string r; int n,d ...

  3. Java学习笔记day08_day09_对象实例化_private_this

    1.类与对象 类就是一个模版. 对象的实例化就是根据模版类, 使用new关键字创建实际的对象. 2.类的定义及创建对象 类的定义格式: public class 类名{ //属性(变量) 数据类型 变 ...

  4. JavaSE---接口

    1.概述 1.1 接口只能继承接口(不能继承类): 1.2 一个接口可以继承多个接口: 1.3 接口中不能包含构造器.初始化块,可以有 属性(只能是常量).方法(只能是抽象方法).内部类(内部接口). ...

  5. 办公开发环境(外接显示屏,wifi热点)

    笔记本电脑怎样外接显示器 https://jingyan.baidu.com/article/3c48dd34495247e10ae35879.html?qq-pf-to=pcqq.c2c 怎样在Wi ...

  6. Java基础02-变量

    1.为什么要使用变量? 变量就是用来记忆数据的,它是一个记忆系统 2.什么是变量? 变量就是一个容器,用来装数据的,变量是放在内存里的. 比如:内存是酒店,变量名就是房间名,变量值就是住进房间的人 3 ...

  7. OneDrive撸5T硬盘空间教程

    注意:要注册多个账户获取网盘的,用无痕模式打开临时教育邮箱网址.打开之后不要关闭,等会用来接收验证码. 1.需要office 365注册这时候需要教育邮箱: 临时教育邮箱:http://sysu.ed ...

  8. 设置webstorm的file watch 监视scss文件

    参考:http://blog.founddrama.net/2013/04/watching-compass-files-in-webstorm/ 上面红色划线部分. 特别注意arguments: 像 ...

  9. Hadoop学习笔记(3) Hadoop文件系统二

    1 查询文件系统 (1) 文件元数据:FileStatus,该类封装了文件系统中文件和目录的元数据,包括文件长度.块大小.备份.修改时间.所有者以及版权信息.FileSystem的getFileSta ...

  10. 二叉排序树思想及C语言实现

    转自: http://blog.chinaunix.net/uid-22663647-id-1771796.html 1.二叉排序树的定义 二叉排序树(Binary Sort Tree)又称二叉查找( ...