zoukankan      html  css  js  c++  java
  • LCT

    就是把树动态轻重链剖分,用splay维护每一条重链啦。

    转载一篇: 本家

    --------------------------------分割线--------------------------------------------

    概念、性质简述

    首先介绍一下链剖分的概念(感谢laofu的讲课)
    链剖分,是指一类对树的边进行轻重划分的操作,这样做的目的是为了减少某些链上的修改、查询等操作的复杂度。
    目前总共有三类:重链剖分,实链剖分和并不常见的长链剖分

    重链剖分

    实际上我们经常讲的树剖,就是重链剖分的常用称呼。
    对于每个点,选择最大的子树,将这条连边划分为重边,而连向其他子树的边划分为轻边。
    若干重边连接在一起构成重链,用树状数组或线段树等静态数据结构维护。
    至于有怎样优秀的性质等等,不在本总结的讨论范畴了(其实是因为本蒟蒻连树剖都不会)

    实链剖分

    同样将某一个儿子的连边划分为实边,而连向其他子树的边划分为虚边。
    区别在于虚实是可以动态变化的,因此要使用更高级、更灵活的Splay来维护每一条由若干实边连接而成的实链。
    基于性质更加优秀的实链剖分,LCT(Link-Cut Tree)应运而生。
    LCT维护的对象其实是一个森林。
    在实链剖分的基础下,LCT资磁更多的操作

    • 查询、修改链上的信息(最值,总和等)
    • 随意指定原树的根(即换根)
    • 动态连边、删边
    • 合并两棵树、分离一棵树(跟上面不是一毛一样吗
    • 动态维护连通性
    • 更多意想不到的操作(可以往下滑一滑)

    想学Splay的话,推荐巨佬yyb的博客


    LCT的主要性质如下:

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

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

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

    各种操作

    access(x)access(x)

    LCT核心操作,也是最难理解的操作。其它所有的操作都是在此基础上完成的。
    因为性质3,我们不能总是保证两个点之间的路径是直接连通的(在一个Splay上)。
    access即定义为打通根节点到指定节点的实链,使得一条中序遍历以根开始、以指定点结束的Splay出现。
    蒟蒻深知没图的痛苦qwq
    所以还是来几张图吧。
    下面的图片参考YangZhe的论文
    有一棵树,假设一开始实边和虚边是这样划分的(虚线为虚边)

    那么所构成的LCT可能会长这样(绿框中为一个Splay,可能不会长这样,但只要满足中序遍历按深度递增(性质1)就对结果无影响)

    现在我们要access(N)access(N),把ANA−N的路径拉起来变成一条Splay。
    因为性质2,该路径上其它链都要给这条链让路,也就是把每个点到该路径以外的实边变虚。
    所以我们希望虚实边重新划分成这样。

    然后怎么实现呢?
    我们要一步步往上拉。
    首先把splay(N)splay(N),使之成为当前Splay中的根。
    为了满足性质2,原来NON−O的重边要变轻。
    因为按深度OO在NN的下面,在Splay中OO在NN的右子树中,所以直接单方面将NN的右儿子置为00(认父不认子)
    然后就变成了这样——

    我们接着把NN所属Splay的虚边指向的II(在原树上是LL的父亲)也转到它所属Splay的根,splay(I)splay(I)。
    原来在II下方的重边IKI−K要变轻(同样是将右儿子去掉)。
    这时候ILI−L就可以变重了。因为LL肯定是在II下方的(刚才LL所属Splay指向了II),所以I的右儿子置为NN,满足性质1。
    然后就变成了这样——

    II指向HH,接着splay(H)splay(H),HH的右儿子置为II。

    HH指向AA,接着splay(A)splay(A),AA的右儿子置为HH。

    ANA−N的路径已经在一个Splay中了,大功告成!
    代码其实很简单。。。。。。循环处理,只有四步——

    1. 转到根;
    2. 换儿子;
    3. 更新信息;
    4. 当前操作点切换为轻边所指的父亲,转1
    inline void access(int x){
        for(int y=0;x;y=x,x=f[x])
            splay(x),c[x][1]=y,pushup(x);//儿子变了,需要及时上传信息
    }

    makeroot(x)makeroot(x)

    只是把根到某个节点的路径拉起来并不能满足我们的需要。更多时候,我们要获取指定两个节点之间的路径信息。
    然而一定会出现路径不能满足按深度严格递增的要求的情况。根据性质1,这样的路径不能在一个Splay中。
    Then what can we do?
    makerootmakeroot定义为换根,让指定点成为原树的根。
    这时候就利用到access(x)access(x)和Splay的翻转操作。
    access(x)access(x)后xx在Splay中一定是深度最大的点对吧。
    splay(x)splay(x)后,xx在Splay中将没有右子树(性质1)。于是翻转整个Splay,使得所有点的深度都倒过来了,xx没了左子树,反倒成了深度最小的点(根节点),达到了我们的目的。
    代码

    inline void pushr(int x){//Splay区间翻转操作
        swap(c[x][0],c[x][1]);
        r[x]^=1;//r为区间翻转懒标记数组
    }
    inline void makeroot(int x){
        access(x);splay(x);
        pushr(x);
    }

    关于pushdown和makeroot的一个相关的小问题详见下方update(关于pushdown的说明)

    findroot(x)findroot(x)

    xx所在原树的树根,主要用来判断两点之间的连通性(findroot(x)==findroot(y)表明x,yx,y在同一棵树中)
    代码:

    inline int findroot(R x){
        access(x); splay(x);
        while(c[x][0])pushdown(x),x=c[x][0];
    //如要获得正确的原树树根,一定pushdown!详见下方update(关于findroot中pushdown的说明)
        splay(x);//此处的问题详见下方update(关于findroot中splay(x)的说明)
        return x;
    }

    同样利用性质1,不停找左儿子,因为其深度一定比当前点深度小。

    split(x,y)split(x,y)

    神奇的makerootmakeroot已经出现,我们终于可以访问指定的一条在原树中的链啦!
    split(x,y)定义为拉出xyx−y的路径成为一个Splay(本蒟蒻以yy作为该Splay的根)
    代码

    inline void split(int x,int y){
        makeroot(x);
        access(y);splay(y);
    }

    x成为了根,那么x到y的路径就可以用access(y)access(y)直接拉出来了,将y转到Splay根后,我们就可以直接通过访问yy来获取该路径的有关信息

    link(x,y)link(x,y)

    连一条xyx−y的边(本蒟蒻使xx的父亲指向yy,连一条轻边)
    代码

    inline bool link(int x,int y){
        makeroot(x);
        if(findroot(y)==x)return 0;//两点已经在同一子树中,再连边不合法
        f[x]=y;
        return 1;
    }

    如果题目保证连边合法,代码就可以更简单[

    inline void link(int x,int y){
        makeroot(x);
        f[x]=y;
    }

    cut(x,y)cut(x,y)

    xyx−y的边断开。
    如果题目保证断边合法,倒是很方便。
    使xx为根后,yy的父亲一定指向xx,深度相差一定是11。当access(y),splay(y)access(y),splay(y)以后,xx一定是yy的左儿子,直接双向断开连接

    inline void cut(int x,int y){
        split(x,y);
        f[x]=c[y][0]=0;
        pushup(y);//少了个儿子,也要上传一下
    }

    那如果不一定存在该边呢?
    充分利用好Splay和LCT的各种基本性质吧!
    正确姿势——先判一下连通性,再看看x,yx,y是否有父子关系,还要看xx是否有右儿子。
    因为access(y)access(y)以后,假如y与x在同一Splay中而没有直接连边,那么这条路径上就一定会有其它点,在中序遍历序列中的位置会介于xx与yy之间。
    那么可能xx的父亲就不是yy了。
    也可能xx的父亲还是yy,那么其它的点就在xx的右子树中,就像这样

    只有三个条件都满足,才可以断掉。

    inline bool cut(int x,int y){
        makeroot(x);
        if(findroot(y)!=x||f[x]!=y||!c[x][1])return 0;
        f[x]=c[y][0]=0;
        pushup(y);
        return 1;
    }

    如果维护了sizesize,还可以换一种判断

    inline bool cut(int x,int y){
        makeroot(x);
        if(findroot(y)!=x&&sz[y]>2)return 0;
        f[x]=c[y][0]=0;
        pushup(y);
    }

    解释一下,如果他们有直接连边的话,access(y)access(y)以后,为了满足性质1,该Splay只会剩下x,yx,y两个点了。
    反过来说,如果有其它的点,sizesize不就大于22了么?


    其实,还有一些LCT中的Splay的操作,跟我们以往学习的纯Splay的某些操作细节不甚相同。
    包括splay(x),rotate(x),nroot(x)splay(x),rotate(x),nroot(x)(看到许多版本LCT写的是isroot(x)isroot(x),但我觉得反过来会方便些)
    这些区别之处详见下面的模板题注释。

    update(关于findroot中pushdown的说明)

    蒟蒻真的一时没注意这个问题。。。。。。Splay根本没学好
    找根的时候,当然不能保证Splay中到根的路径上的翻转标记全放掉。
    所以最好把pushdown写上。
    Candy巨佬的总结对pushdown问题有详细的分析
    只不过蒟蒻后来经常习惯这样判连通性(我也不知道怎么养成的

    makeroot(x);
    if(findroot(y)==x)//后续省略

    这样好像没出过问题,那应该可以证明是没问题的(makeroot保证了x在LCT的顶端,access(y)+splay(y)以后,假如x,y在一个Splay里,那x到y的路径一定全部放完了标记)
    导致很久没有发现错误。。。。。。
    另外提一下,假如LCT题目在维护连通性的情况中只可能出现合并而不会出现分离的话,其实可以用并查集哦!(实践证明findroot很慢)
    这样的例子有不少,比如下面“维护链上的边权信息”部分的两道题都是的。
    甚至听到Julao们说有少量题目还专门卡这个细节。。。。。。XZY巨佬的博客就提到了(我太弱啦,暂时并不会

    update(关于pushdown的说明)

    我pushdown和makeroot有时候会这样写,常数小一点

    void pushdown(int x){
        if(r[x]){
            r[x]=0;
            int t=c[x][0];
            r[c[x][0]=c[x][1]]^=1;
            r[c[x][1]=t]^=1;
        }
    }
    void makeroot(int x){
        access(x);splay(x);
        r[x]^=1;
    }

    这种写法等于说当x有懒标记时,x的左右儿子还是反的
    那么如果findroot里实在要写pushdown,那么这种pushdown就会出现问题(参考评论区@ zjp_shadow巨佬的指正)
    所以此总结以及下面模板里的pushdown,常数大了一点点,却是更稳妥、严谨的写法

    //pushr同上方makeroot部分
    void pushdown(int x){
        if(r[x]){
            if(c[x][0])pushr(c[x][0]);//copy自模板,然后发现if可以不写
            if(c[x][1])pushr(c[x][1]);
            r[x]=0;
        }
    }
    void makeroot(int x){
        access(x);splay(x);
        pushr(x);//可以看到两种写法造成makeroot都是不一样的
    }

    这种写法等于说当x有懒标记时,x的左右儿子已经放到正确的位置了,只是儿子的儿子还是反的
    那么这样就不会出问题啦
    两种写法差别还确实有点大呢
    当题目中维护的信息与左右儿子顺序有关的时候,pushdown如果用这种不严谨写法会是错的
    比如[NOI2005]维护数列(这是Splay题)和洛谷P3613 睡觉困难综合征

    update(关于findroot中splay(x)的说明)

    某位Julao指出findroot中在找到原树根后(此时x跳到了原树根)应splay(x),伸展一下,Splay的特性,保证复杂度(好像牵涉到玄学的势能分析,蒟蒻什么也不会啊QvQ)
    非常正确的做法。于是本蒟蒻进行了更正,却忘记了进行验证。
    后来Destinies巨佬指出第8个点WA。
    经过验证之后发现,加上splay(x)以后,点的相对位置发生了变化,导致cut需要更改,更改如下:

    I cut(R x,R y){//断边
        makeroot(x);
        if(findroot(y)==x&&f[y]==x&&!c[y][0]){
            f[y]=c[x][1]=0;//x在findroot(y)后被转到了根
            pushup(x);
        }
    }

    为了避免频繁讨论、修改带来的繁琐,此总结不建议在此模板题里加上splay(x)
    因为确实很难找到卡掉不写splay(x)的代码的数据,而且可能带来一点常数。
    或许我大多数时候把splay写成单旋(没错就是HNOI2017那种)会比Zig、Zag双旋要快个十几分之一也是这样的道理吧。。。。。。
    但这不意味着就不用写了
    在比较关键的时候(比如比赛时)该写的总要写。
    不管是单旋,还是不splay(x),都是很容易卡掉的。。。。。。
    相信Dalao们都能熟练地在很多种不同的写法中切换的

    --------------------------------分割线--------------------------------------------

    放个模板哈:

    #include<bits/stdc++.h>
    #define N 300005
    using namespace std;
    #define sight(x) ('0'<=x&&x<='9')
    inline void read(int &x){
        static char c;static int b;
        for (b=1,c=getchar();!sight(c);c=getchar())if (c=='-') b=-1;
        for (x=0;sight(c);c=getchar())x=x*10+c-48; x*=b;
    }
    void write(int x){if (x<10) {putchar('0'+x); return;} write(x/10); putchar('0'+x%10);}
    inline void writeln(int x){ if (x<0) putchar('-'),x*=-1; write(x); putchar('
    '); }
    inline void writel(int x){ if (x<0) putchar('-'),x*=-1; write(x); putchar(' '); }
    int anw[N],val[N],f[N],ch[N][2],z,y,q[N],top,rev[N],kind,n,m,qq,x;
    inline void push_up(int x){
        anw[x]=val[x]^anw[ch[x][0]]^anw[ch[x][1]];
    }
    inline void push_down(int x){
        if (rev[x]) {
            rev[ch[x][0]]^=1; rev[ch[x][1]]^=1; rev[x]=0;
            swap(ch[x][0],ch[x][1]);
        }
    }
    inline bool rt(int x){
        return ch[f[x]][0]!=x&&ch[f[x]][1]!=x;
    }
    inline void ro(int x){
        int y=f[x],z=f[y];
        kind=(ch[y][0]==x);
        if (!rt(y)) ch[z][ch[z][1]==y]=x;
        f[ch[x][kind]]=y; f[y]=x; f[x]=z;
        ch[y][kind^1]=ch[x][kind]; ch[x][kind]=y;
        push_up(y); push_up(x);
    }
    inline void splay(int x) {
        top=1;
        q[top]=x;
        for (int i=x;!rt(i);i=f[i]) q[++top]=f[i];
        while (top) push_down(q[top--]);
        while (!rt(x)) {
            int y=f[x],z=f[y];
            if (!rt(y)) 
            {
               if (ch[y][0]==x^ch[z][0]==y) ro(x); 
               else ro(y);
            }
            ro(x);
        }
    //    push_up(x);
    }
    inline void access(int x){
        for (int t=0;x;t=x,x=f[x]) splay(x),ch[x][1]=t,push_up(x);
    }
    inline void make_rt(int x){
        access(x); splay(x); rev[x]^=1; //
    }
    inline void split(int x,int y){
        make_rt(x); access(y); splay(y);
    }
    inline void link(int x,int y){
        make_rt(x); f[x]=y;
    }
    inline int find(int x) {
        access(x); splay(x); while(ch[x][0]) push_down(x),x=ch[x][0]; return x;
    }
    inline void cut(int x,int  y){//断边
        make_rt(x);
        if(find(y)==x&&f[x]==y&&!ch[x][1]){
            f[x]=ch[y][0]=0; push_up(y);
        }
    }
    signed main () {
        read(n); read(m);
        for (int i=1;i<=n;i++) read(val[i]);
        while (m--) {
            read(qq); read(x); read(y);
            switch(qq) {
                case 0: split(x,y); writeln(anw[y]);break;
                case 1: if (find(x)^find(y)) link(x,y);break;
                case 2: if (find(x)==find(y)) cut(x,y); break;
                case 3: splay(x); val[x]=y;  break;
            }
        } return 0;
    }
  • 相关阅读:
    ubuntu切换中英文通用方法,ubuntu中文语言
    ubuntu安装ibus-goolepinyin通用方法
    ubuntu12.04 64位系统配置jdk1.6和jdk-6u20-linux-i586.bin下载地址
    ubuntu创建桌面快捷方式
    vim记住上次编辑和浏览位置
    ubuntu12.04安装tftp,配置,修改目录,错误类型
    Ubuntu 12.04 make menuconfig 出现 Unable to find the ncurses libraries or the required header files.
    nginx六 之Session共享
    nginx五 之高可用
    nginx四 之缓存模块
  • 原文地址:https://www.cnblogs.com/rrsb/p/8710413.html
Copyright © 2011-2022 走看看