zoukankan      html  css  js  c++  java
  • Link Cut Tree

    想学LCT很久了,但自己看博客又没动力,前天国家队任轩笛大佬莅临讲课,虽然只是大致地带过了一下LCT,但是有了国家队大佬的BUFF就是不一样(%%%)马上来了信仰学习一波LCT。


    【什么是LCT】

    LCT即Link Cut Tree 动态树

    这个数据结构支持对树的形状进行修改,比如连边和删边。

    那么我们怎么实现呢,很显然我们需要一个可以容易改变形状的数据结构——>splay强势登场

    来张经典图,学LCT怎么能不看这张图!!!

    我们的LCT就是多个splay组成的,每个splay都可以管理一条链,每条splay的根节点的父亲都不再是null了,而是另外splay的某个节点,但是当前节点记录了父亲,但是父亲并不记录这个儿子(所谓子认父不认,听起来是不是有点残忍),所以这条边就是一条虚边,而splay内部的边为实边。

    LCT的主要性质如下:

      1. 每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增。
        是不是有点抽象哈
        比如有一棵树,根节点为1(深度1),有两个儿子2,3(深度2),那么Splay有3种构成方式:
        {12},{3}
        {13},{2}
        {1},{2},{3}(每个集合表示一个Splay)
        而不能把1,2,3同放在一个Splay中(存在深度相同的点)

      2. 每个节点包含且仅包含于一个Splay中

      3. 边分为实边和虚边,实边包含在Splay中,而虚边总是由一棵Splay指向另一个节点(指向该Splay中中序遍历最靠前的点在原树中的父亲)。
        因为性质2,当某点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个Splay中的。
        那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的Splay的根节点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。

    前置知识:(其实觉得并不需要什么前置知识)但大佬们都说要先学树剖,那就学呗。

    学过树剖的人都知道,树剖是进行重链剖分,而LCT则是进行实链剖分。

    【怎么剖分???】

    此处应该有图了,不然就要懵逼了。。。

    如图,实线为实边,虚线为虚边。

     那么我们就将由实边连成的链放在一个splay,值得注意的是实边并不固定,我们可以将其改为实边或虚边,不然怎么实现改变树的形状。

    来看看这棵树变成LCT后是什么样:(绿色方框表示一个splay)

    【LCT基本操作】

    1.access (关键操作,LCT精髓

    这个操作j是将从某个节点到树的根节点的路径全改为实链(这里不是splay的根节点而是整棵树的)就类似于打通任督二脉一样,是不是贼帅。

    那么怎么实现呢?

    我们所要做的就是把这个节点所在splay的父节点的实链改为虚链,然后又把父节点到这个节点的虚链改为实链,但注意,首先要把这个节点的实链断掉

    我们每次先将这个点转到当前splay的根,然后把他父亲的右儿子改为当前节点,就这样一直跳到树根。

    如图所示(我们access(N)):

    1 void access(int v)
    2 {
    3     for(int y=0;v;v=t[y=v].fa)
    4     {
    5         splay(v);
    6         t[v].son[1]=y;update(v);
    7     }
    8 }

    2.换根操作

    我们可以把一个元素换为这棵树的根

     我们只需要一次access,再加splay就可以将这个点转到树根所在的splay的根节点,但并不是这棵树的根,因为这棵树的根在左儿子(深度最浅的点)

    但是我们会发现由于这个点是靠一个access连上去的,所以它一定是这棵splay最深的点,没错吧,所以我们只需要一次区间翻转即可将其换为根节点。

    1 void mroot(int v) 
    2 {
    3     access(v);
    4     splay(v);
    5     turn(v);//打上区间翻转标记
    6 }

    掌握了上述两种基本操作后就可以通过上述两种操作进行LCT的操作了

    3.findroot 操作

    我们要找一个点所在树的树根

    首先将当前节点到根的路径打通,然后splay一路转上去,然后我们反复查左儿子就找到根了,顺便下传一下翻转标记

    1 int findroot(int v)
    2 {
    3     access(v),splay(v),push(v);
    4     while(t[v].son[0]) v=t[v].son[0];
    5     return v;
    6 }

    4.split操作

    这个是来提取树上的一条链

    我们首先将节点A改为树的根节点,然后又将节点B转上去,发现树根一定就在节点B的左子树中。

    1 void split(int a,int b) {mroot(a),access(b),splay(b);}

    5.link 操作

    这里首先要看题目要求连边是不是合法

    如果一定合法,那很好我们只需要将一个节点转到树根,然后将这个节点的父节点指向另一个节点,相当于连了一条虚边

    1 void link(int a,int b) {mroot(a),t[a].fa=b;}

    如果不一定合法,那也好办,我们就判一判这两个点是不是在一棵树里,再连边

    1 void link(int a,int b) 
    2 {
    3     mroot(a);
    4     if(findroot(b)!=a) t[a].fa=b;
    5 }

    6.cut操作

    首先还是要看删边是不是合法

    如果一定合法,显然提取链之后就将splay根节点和左儿子断开就好

    1 void cut(int a,int b) 
    2 {
    3     split(a,b);
    4     t[a].fa=t[b].son[0]=0;
    5 }

    如果不一定,那就判一判

    1 void cut(int a,int b) 
    2 {
    3     mroot(a);
    4     if(findroot(b)==a&&t[a].fa==b&&!t[a].son[1])
    5     t[a].fa=t[b].son[0]=0,update(b);
    6 }

    7.最后是经典的splay操作

     1 void rotate(int v)
     2 {
     3     int ff=t[v].fa,gff=t[ff].fa,which=getson(v);
     4     if(!isroot(ff)) t[gff].son[t[gff].son[1]==ff]=v;
     5     t[ff].son[which]=t[v].son[1-which],t[ff].fa=v;
     6     if(t[v].son[1-which]) t[t[v].son[1-which]].fa=ff;
     7     t[v].son[1-which]=ff,t[v].fa=gff;
     8     update(ff),update(v);//如果需要维护的话
     9 }
    10 void splay(int v)
    11 {
    12     push(v);
    13     while(!isroot(v))
    14     {
    15         if(!isroot(t[v].fa))
    16         rotate((getson(t[v].fa)==getson(v))?t[v].fa:v);
    17         rotate(v);
    18     }  
    19     update(v);//如果需要维护的话
    20 }

    来看一道板子题

    P2147 [SDOI2008]洞穴勘测

    【代码实现】

     1 #include<cstdio>
     2 #include<iostream>
     3 using namespace std;
     4 const int maxn=10005;
     5 struct sd{
     6     int son[2],fa,rev;
     7 }t[maxn];
     8 bool getson(int v) {return t[t[v].fa].son[1]==v;}
     9 bool isroot(int v) {return t[t[v].fa].son[0]!=v&&t[t[v].fa].son[1]!=v;}
    10 void update(int v)
    11 {
    12     if(!v) return;
    13     swap(t[v].son[0],t[v].son[1]);
    14     t[v].rev^=1;
    15 }
    16 void pushdown(int v)
    17 {
    18     if(t[v].rev)
    19     update(t[v].son[0]),update(t[v].son[1]),t[v].rev=0;
    20 }
    21 void push(int v)
    22 {
    23     if(!isroot(v)) push(t[v].fa);
    24     pushdown(v);
    25 }
    26 void rotate(int v)
    27 {
    28     int ff=t[v].fa,gff=t[ff].fa,which=getson(v);
    29     if(!isroot(ff)) t[gff].son[t[gff].son[1]==ff]=v;
    30     t[ff].son[which]=t[v].son[1-which],t[ff].fa=v;
    31     if(t[v].son[1-which]) t[t[v].son[1-which]].fa=ff;
    32     t[v].son[1-which]=ff,t[v].fa=gff;
    33 }
    34 void splay(int v)
    35 {
    36     push(v);
    37     while(!isroot(v))
    38     {
    39         if(!isroot(t[v].fa))
    40         rotate((getson(t[v].fa)==getson(v))?t[v].fa:v);
    41         rotate(v);
    42     }    
    43 }
    44 void access(int v)
    45 {
    46     for(int y=0;v;v=t[y=v].fa)
    47     {
    48         splay(v);
    49         t[v].son[1]=y;
    50     }
    51 }
    52 void mroot(int v)
    53 {
    54     access(v);
    55     splay(v);
    56     update(v);
    57 }
    58 void link(int a,int b) {mroot(a),t[a].fa=b;}
    59 void cut(int a,int b) 
    60 {
    61     mroot(a),access(b),splay(b);
    62     t[a].fa=t[b].son[0]=0;
    63 }
    64 int findroot(int v)
    65 {
    66     access(v),splay(v),push(v);
    67     while(t[v].son[0]) v=t[v].son[0];
    68     return v;
    69 }
    70 int main()
    71 {
    72     int n,m;
    73     scanf("%d%d",&n,&m);
    74     char ord[50];
    75     for(int i=1;i<=m;i++)
    76     {
    77         int a,b;
    78         scanf("%s%d%d",ord,&a,&b);
    79         if(ord[0]=='Q') if(findroot(a)==findroot(b)) printf("Yes
    ");else printf("No
    ");
    80         if(ord[0]=='C') link(a,b);
    81         if(ord[0]=='D') cut(a,b);
    82     }
    83     return 0;
    84 }
  • 相关阅读:
    数据库常用操作命令以及explain执行计划
    spring中父子容器
    为什么SpringCloud引导类不加@EnableDiscoveryClient也可以注册到eureka中
    使用dubbo的注解,AOP配置xml的方式无法开启事务
    Excel导出打印失败报错 (eg HSSF instead of XSSF)
    0317 ajax
    0316 事务
    0316 DBUtils
    0315 el技术和jstl技术 javaEE开发模式
    0313 jsp
  • 原文地址:https://www.cnblogs.com/genius777/p/9036812.html
Copyright © 2011-2022 走看看