树上数据结构——LCT

Posted lcyfrog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树上数据结构——LCT相关的知识,希望对你有一定的参考价值。

树上数据结构——LCT

概述

LCT是一种强力的树上数据结构,支持以下操作:

  1. 链上求和
  2. 链上求最值
  3. 链上修改
  4. 子树修改
  5. 子树求和
  6. 换根
  7. 断开树上一条边
  8. 连接两个点,保证连接后仍然是一棵树。

基本概念

LCT是对树的实链剖分,即把所有边划分为实边和虚边

类似于重链剖分,每个点连向子节点中的实链至多只会有一条,把这条实边连向的儿子叫做实儿子

把一些实边连接的点构成的链叫做实链,容易发现实链之间没有共同点

需要注意的是一个不在实边上的点(一些叶节点)也视为一条没有实边的实链

于是实链之间一定是用虚边链接的

要涉及动态删连边操作,于是使用splay来维护一条实链,splay是LCT的辅助树

此处splay的深度按中序遍历严格递增

由于用splay维护,LCT的实边是动态的,可以改变

核心操作

? access(x):让x到根节点的所有边均为实边,并且x没有实儿子

这个推荐flash_hu的博客,简单易懂

稍微说一下,每次操作先把当前要连的点splay到当前splay的根,由于splay中深度按中序遍历递增,此时根的右儿子一定是之前连的实链,需要去掉

于是把之前的点连到当前根的右儿子就行了

注意此时一些\\(fa,son,isroot\\)之类的信息改变了,需要\\(push\\)_\\(up\\)

void access(int x)
    for(int y=0;x;y=x,x=fa[x]) //y是之前的根,x是当前需要连的点
        splay(x); ch[x][1]=y;
        push_up(x);
    

其他操作

  1. makeroot

    换根操作

    access(x)之后x是深度最大的点

    所以splay(x)之后,x在splay中一定没有右子树,这个时候翻转整个splay,所有点的深度就都倒过来了,x成为深度最小的点,即为根节点

    void pushr(int x)
        swap(ch[x][0],ch[x][1]);
        r[x]^=1;
    
    void makeroot(int x)
        access(x); splay(x);
        pushr(x);
    
  2. findroot

    找所在树的树根,可以用来判断两点之间的连通性(两点所在树相同则有唯一相同根

    int findroot(int x)
        access(x);splay(x);
        while(c[x][0]) push_down(x),x=ch[x][0];//寻找深度最小的点,此处push_down是为了x到跟的标记放完,好判连通性
        splay(x);//多多splay有益健康
        return 0;
    
  3. split

    把一条路径拉成一个splay

    void spilt(int x,int y)
        makeroot(x);access(y);
        splay(y);
    
  4. link

    连一条边,保证连完还是一棵树

    不保证合法:

    int link(int x,int y)
        makeroot(x);
        if(findroot(y)==x) return 0;
        fa[x]=y; //把x作为y的儿子
        return 1;
    

    保证合法:

    void link(int x,int y)
        makeroot(x);
        fa[x]=y;
    

    此处连的边是虚边(感受到实链剖分的方便了罢

  5. cut

    断边

    保证存在:

    void cut(int x,int y)
        split(x,y);
        fa[x]=ch[y][0]=0;
    
    

    不存在此边的时候是什么情况呢?

    先把x给\\(makeroot\\)到根

    1. x和y不连通 (\\(findroot\\)

    2. 在同一splay中而没有直接连边 (\\(f[y]==x\\)\\(!c[y][0]\\))

      (考虑其他的点在哪里,在findroot之后x到了根节点,如果x和y之间有点,只能是在y到根的路径上或者y的左儿子上)

    int cut(int x,int y)
        makeroot(x);
        if(findroot(y)!=x||fa[y]!=x||ch[y][0]) return 0;
        fa[y]=ch[x][1]=0;
        push_up(x);
        return 1;
    
  6. nroot

    naiive的操作,判断此点是否不是当前splay的根节点

    int nroot(int x)
        return (ch[fa[x]][1]==x||ch[fa[x]][0]==x);
    
  7. splay 的特殊性

    此处splay的标记一定要从上往下放,也就是先开个栈把标记放完再旋转

完整模板

#include<bits/stdc++.h>
#define R register int
#define I inline void
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
#define lc c[x][0]
#define rc c[x][1]
using namespace std;
const int SZ=1<<19,N=3e5+9;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in()
    G;while(*ip<'-')G;
    R x=*ip&15;G;
    while(*ip>'-')x*=10;x+=*ip&15;G;
    return x;

int f[N],c[N][2],v[N],s[N],st[N];
bool r[N];
inline bool nroot(R x)//判断节点是否为一个Splay的根(与普通Splay的区别1)
    return c[f[x]][0]==x||c[f[x]][1]==x;
//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
I pushup(R x)//上传信息
    s[x]=s[lc]^s[rc]^v[x];

I pushr(R x)R t=lc;lc=rc;rc=t;r[x]^=1;//翻转操作
I pushdown(R x)//判断并释放懒标记
    if(r[x])
        if(lc)pushr(lc);
        if(rc)pushr(rc);
        r[x]=0;
    

I rotate(R x)//一次旋转
    R y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
    if(nroot(y))c[z][c[z][1]==y]=x;c[x][!k]=y;c[y][k]=w;//额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
    if(w)f[w]=y;f[y]=x;f[x]=z;
    pushup(y);

I splay(R x)//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
    R y=x,z=0;
    st[++z]=y;//st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
    while(nroot(y))st[++z]=y=f[y];
    while(z)pushdown(st[z--]);
    while(nroot(x))
        y=f[x];z=f[y];
        if(nroot(y))
            rotate((c[y][0]==x)^(c[z][0]==y)?x:y);
        rotate(x);
    
    pushup(x);

/*当然了,其实利用函数堆栈也很方便,代替上面的手工栈,就像这样
I pushall(R x)
    if(nroot(x))pushall(f[x]);
    pushdown(x);
*/
I access(R x)//访问
    for(R y=0;x;x=f[y=x])
        splay(x),rc=y,pushup(x);

I makeroot(R x)//换根
    access(x);splay(x);
    pushr(x);

int findroot(R x)//找根(在真实的树中的)
    access(x);splay(x);
    while(lc)pushdown(x),x=lc;
    splay(x);
    return x;

I split(R x,R y)//提取路径
    makeroot(x);
    access(y);splay(y);

I link(R x,R y)//连边
    makeroot(x);
    if(findroot(y)!=x)f[x]=y;

I cut(R x,R y)//断边
    makeroot(x);
    if(findroot(y)==x&&f[y]==x&&!c[y][0])
        f[y]=c[x][1]=0;
        pushup(x);
    

int main()

    R n=in(),m=in();
    for(R i=1;i<=n;++i)v[i]=in();
    while(m--)
        R type=in(),x=in(),y=in();
        switch(type)
        case 0:split(x,y);printf("%d\\n",s[y]);break;
        case 1:link(x,y);break;
        case 2:cut(x,y);break;
        case 3:splay(x);v[x]=y;//先把x转上去再改,不然会影响Splay信息的正确性
        
    
    return 0;

之后可能会补自己做的LCT题(咕


在创作本文的过程中,参考了以下文章:

以上是关于树上数据结构——LCT的主要内容,如果未能解决你的问题,请参考以下文章

lct树上查询最大值和路径长

洛谷P3348 [ZJOI2016]大森林(LCT,虚点,树上差分)

BZOJ 2631 tree 动态树(Link-Cut-Tree)

芝士:LCT

关于动态树和LCT的一些学习感受(持续更新)

「CF827D Best Edge Weight」 - LCT