前言

\(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\)是认父不认子的,所以只需要判断当前节点的父亲节点的两个子节点是否为当前节点即可。

    代码如下:

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

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

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

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

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

    具体代码如下:

  1. #define Which(x) (node[node[x].Father].Son[1]==x)
  2. #define Connect(x,y,d) (node[node[x].Father=y].Son[d]=x)
  3. inline void Rotate(int x)
  4. {
  5. register int fa=node[x].Father,pa=node[fa].Father,d=Which(x);
  6. !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);
  7. }
  8. inline void Splay(int x)
  9. {
  10. register int fa=x,Top=0;
  11. while(Stack[++Top]=fa,!IsRoot(fa)) fa=node[fa].Father;//存入栈中
  12. while(Top) PushDown(Stack[Top]),--Top;//依次PushDown
  13. while(!IsRoot(x)) fa=node[x].Father,!IsRoot(fa)&&(Rotate(Which(x)^Which(fa)?x:fa),0),Rotate(x);
  14. }
  • \(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\)最核心的操作,也是后面许多操作的基础。

    具体实现可以详见代码:

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

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

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

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

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

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

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

    代码如下:

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

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

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

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

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

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

    代码如下:

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

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

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

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

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

    代码如下:

  1. inline void Link(int x,int y)//在x和y两个节点间连一条边
  2. {
  3. MakeRoot(x),FindRoot(y)^x&&(node[x].Father=y);//判断x和y的连通性,然后连接
  4. }
  • \(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\)必然不相连。

    代码如下:

  1. inline void Cut(int x,int y)//删除x和y之间的边
  2. {
  3. 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的连通性,然后删边
  4. }
  • \(Split(x,y)\)

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

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

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

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

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

    代码如下:

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

模板(板子题

  1. #include<bits/stdc++.h>
  2. #define N 300000
  3. #define swap(x,y) (x^=y^=x^=y)
  4. using namespace std;
  5. int n,a[N+5];
  6. class Class_FIO
  7. {
  8. private:
  9. #define Fsize 100000
  10. #define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,Fsize,stdin),A==B)?EOF:*A++)
  11. #define pc(ch) (FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,Fsize,stdout),Fout[(FoutSize=0)++]=ch))
  12. int Top,FoutSize;char ch,*A,*B,Fin[Fsize],Fout[Fsize],Stack[Fsize];
  13. public:
  14. Class_FIO() {A=B=Fin;}
  15. inline void read(int &x) {x=0;while(!isdigit(ch=tc()));while(x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));}
  16. inline void writeln(int x) {while(Stack[++Top]=x%10+48,x/=10);while(Top) pc(Stack[Top--]);pc('\n');}
  17. inline void clear() {fwrite(Fout,1,FoutSize,stdout),FoutSize=0;}
  18. }F;
  19. class Class_LCT
  20. {
  21. private:
  22. #define LCT_SIZE N
  23. #define PushUp(x) (node[x].Sum=node[x].Val^node[node[x].Son[0]].Sum^node[node[x].Son[1]].Sum)
  24. #define Rever(x) (swap(node[x].Son[0],node[x].Son[1]),node[x].Rev^=1)
  25. #define PushDown(x) (node[x].Rev&&(Rever(node[x].Son[0]),Rever(node[x].Son[1]),node[x].Rev=0))
  26. #define Which(x) (node[node[x].Father].Son[1]==x)
  27. #define Connect(x,y,d) (node[node[x].Father=y].Son[d]=x)
  28. #define IsRoot(x) (node[node[x].Father].Son[0]^x&&node[node[x].Father].Son[1]^x)
  29. #define MakeRoot(x) (Access(x),Splay(x),Rever(x))
  30. #define Split(x,y) (MakeRoot(x),Access(y),Splay(y))
  31. int Stack[LCT_SIZE+5];
  32. struct Tree
  33. {
  34. int Val,Sum,Father,Rev,Son[2];
  35. }node[LCT_SIZE+5];
  36. inline void Rotate(int x)
  37. {
  38. register int fa=node[x].Father,pa=node[fa].Father,d=Which(x);
  39. !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);
  40. }
  41. inline void Splay(int x)
  42. {
  43. register int fa=x,Top=0;
  44. while(Stack[++Top]=fa,!IsRoot(fa)) fa=node[fa].Father;
  45. while(Top) PushDown(Stack[Top]),--Top;
  46. while(!IsRoot(x)) fa=node[x].Father,!IsRoot(fa)&&(Rotate(Which(x)^Which(fa)?x:fa),0),Rotate(x);
  47. }
  48. 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);}
  49. 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;}
  50. public:
  51. inline void Init(int len,int *data) {for(register int i=1;i<=len;++i) node[i].Val=data[i];}
  52. inline void Link(int x,int y) {MakeRoot(x),FindRoot(y)^x&&(node[x].Father=y);}
  53. 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));}
  54. inline void Update(int x,int v) {Splay(x),node[x].Val=v;}
  55. inline int Query(int x,int y) {return Split(x,y),node[y].Sum;}
  56. }LCT;
  57. int main()
  58. {
  59. register int query_tot,i,op,x,y;
  60. for(F.read(n),F.read(query_tot),i=1;i<=n;++i) F.read(a[i]);
  61. for(LCT.Init(n,a);query_tot;--query_tot)
  62. {
  63. F.read(op),F.read(x),F.read(y);
  64. switch(op)
  65. {
  66. case 0:F.writeln(LCT.Query(x,y));break;
  67. case 1:LCT.Link(x,y);break;
  68. case 2:LCT.Cut(x,y);break;
  69. case 3:LCT.Update(x,y);break;
  70. }
  71. }
  72. return F.clear(),0;
  73. }

后记

推荐一些比较好的\(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. HIVE分析函数

    ROWS BETWEEN含义,也叫做WINDOW子句: PRECEDING:往前 FOLLOWING:往后 CURRENT ROW:当前行 UNBOUNDED:起点,UNBOUNDED PRECEDI ...

  2. node服务器端模块化-commomjs

    modele.js getmodule.js 用exports 返回的是一个对象中的每个属性

  3. spring 事务 @EnableTransactionManagement原理

    @EnableXXX原理:注解上有个XXXRegistrar,或通过XXXSelector引入XXXRegistrar,XXXRegistrar实现了 ImportBeanDefinitionRegi ...

  4. Maven使用之packing篇

    项目的打包类型:pom.jar.war 项目中一般使用maven进行模块管理,每个模块下对应都有一个pom文件,pom文件中维护了各模块之间的依赖和继承关系.项目模块化可以将通用的部分抽离出来,方便重 ...

  5. c++11 move构造函数和move operator 函数 学习

    先看个代码吧!!!!!!!!!! #include <iostream> using namespace std; class A { public: A(){cout<<&q ...

  6. 百度webuploader 上传演示例子

    前端代码 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="baiduWebU ...

  7. 2019.03.21 读书笔记 基元类型的Parse与TryParse 性能与建议

    Parse转换失败时会抛出异常,耗损性能,如果转换成功,则与TryParse无差异.查看源码,tryparse的代码更多一些,在失败时,反而性能更优,主要是抛出异常耗损了性能.所以在不确定是用Tryp ...

  8. 注册中心eureka

    最近在忙一些其它的事情,两个城市来回跑还要办一些手续,挺费劲的,学习的事情也就耽误了一些,尽量赶吧. spring cloud为分布式的微服务架构提供了一站式的解决方案,eureka注册中心在spri ...

  9. 关于Mysql数据库的注意点

    1.注意属性为String的数据在JDBC操作语句中要加单引号 例子: conn = DriverManager.getConnection("jdbc:mysql://localhost: ...

  10. Junit使用过程中需要注意的诡异bug以及处理办法

    在开发过程中我们有时会遇到狠多的问题和bug,对于在编译和运行过程中出现的问题很好解决,因为可以在错误日志中得到一定的错误提示信息,从而可以找到一些对应的解决办法.但是有时也会遇到一些比较诡异的问题和 ...