LCT (Link-Cut-Tree) 学习笔记

LCT

解决动态树问题的一种数据结构

推荐 PoPoQQQ 的详解

链上求和
链上求最值
链上修改
断开树上一条边 *
连接两个点 *


概念
  • 重儿子(暂时这样命名): 一个节点最多只能有一个重儿子
  • 重边: 连接父亲节点和重儿子的边
  • 重链: 由重边和重边连接的节点构成的链
  • 轻边: 除了重边以外的边

重点
  • 类比树链剖分,每条重链都用线段树维护信息, LCT用splay来维护(因为它的结构在不断变化)
  • splay中每个节点的键值是它的深度(不用单独记录和比较)
  • 一条重链对应一个splay树,不同重链的splay相互分离
  • splay的树根不代表原树的树根,它只是splay树的一个节点
  • splay就是一条重链,重链的顶部的父节点记录在splay的根上
  • 每个点的记录的父节点不意味着原树的父节点(除了每个splay根记录的)
  • splay的中序遍历就是重链从顶部到底部的序列
  • LCT的根在不停的变化

各种操作
access
功能: 切掉与重儿子的重边,建立一条通向原树根的重链
用途: 形成当前点到原树根的重链的splay,快速维护链上信息
  • 先将x旋转到splay的根部
  • 将它的重儿子换成新的重儿子,也就是上一个重链的顶部
  • 重复操作它的父节点
void access(int x) {
    for(int y=0; x; y=x, x=fa(x)){
        splay(x);
        //现在x的重儿子一定是它的rson,证明 在后面
        rson(x)=y; update(x); 
    }
}

y表示上一个操作的x

makeRoot
功能: 将x变成原树根
用途: 方便的找到x->y的链(先将x变为根,再access(y))
  • 先让x与原树根联通(access(x))
  • splay(x) 让它转到根部
  • reverse

reverse之前

reverse之前

reverse之后

reverse之后

也就是反转操作,保证在splay中相对位置的正确(交换每一个节点的左右儿子)
这里是标记,在splay时要降标记

void reverse(int x) {
    if(!x) return;
    swap(lson(x), rson(x)); T[x].reverse^=1;
}


void makeRoot(int x) {
    access(x); splay(x); reverse(x);
}
功能:连接x->y
  • 将x变成原树根
  • 将x的父节点记为y
void link(int x, int y) {
    if(findRoot(x)==findRoot(y)) return;
    makeRoot(x); fa(x)=y;
}
cut
功能: 切掉x->y的连边
  • 先将x变为原树根
  • 再access(y),将x与y连在一起(此时splay中只有两个节点)
  • splay(y), 此时 y 的左儿子就是x,证明在后面
  • 清空x的父节点,y的右儿子
void cut(int x, int y) {
    makeRoot(x); access(y); splay(y);
    fa(x)=lson(y)=0;
}
splay部分

int getson(int x) { return x==rson(fa(x)); }
bool is_root(int x) { return lson(fa(x))!=x && rson(fa(x))!=x; }//判断是否为当前splay的根
void update(int x) { ... }
void reverse(int x) {
    if(!x) return;
    swap(lson(x), rson(x)); T[x].reverse^=1;
}
void downFlag(int x) {
    if(!is_root(x)) downFlag(fa(x));
    if(T[x].reverse){
        reverse(lson(x)); reverse(rson(x));
        T[x].reverse=0;
    }
}
void rotate(int x) {
    int fa=fa(x), gfa=fa(fa), wh=getson(x);
    if(!is_root(fa)) T[gfa].ch[getson(fa)]=x; fa(x)=gfa;
    T[fa].ch[wh]=T[x].ch[wh^1]; T[T[fa].ch[wh]].fa=fa;
    T[x].ch[wh^1]=fa; T[fa].fa=x;
    update(fa); update(x);
}
void splay(int x) {
    downFlag(x);
    for(; !is_root(x); rotate(x))
        if(!is_root(fa(x)))
            rotate(getson(fa(x))==getson(x)?fa(x):x);
}

PoPoQQQ
由于找节点并非自上至下,故操作之前需预先将节点到splay根的标记全下传一遍
由于父节点未必和当前节点在同一棵Splay中,所以要判断是不是父亲节点的儿子(is_root)

注:证明部分

PoPoQQQ
反证法
假设y进行Access+Splay操作时通过双旋将x旋转到y的左儿子的左儿子,那么x和y之间一定有一个节点,故x,y之间深度差不为1,与已知x,y之间有连边矛盾

模板题

  1. [SDOI2008]Cave 洞穴勘测
  2. [模板]Link Cut Tree (动态树)
  3. [HNOI2010]Bounce 弹飞绵羊

冗长代码

    /*
    [HNOI2010]Bounce 弹飞绵羊
    */
    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    #include <cmath>
    #define File(x) "bzoj_2002."#x
    #define For(i,s,e) for(int i=(s); i<=(e); i++)
    #define Rep(i,s,e) for(int i=(s); i>=(e); i--)
    using namespace std;

    const int N=200000+3;

    struct LCT{
        int ch[2],fa,size;
        bool reverse;
        #define lson(x) T[x].ch[0]
        #define rson(x) T[x].ch[1]
        #define fa(x) T[x].fa
    }T[N];

    int n,k[N],m,t;

    bool is_root(int x) { return lson(fa(x))!=x && rson(fa(x))!=x; }
    int getson(int x) { return x==rson(fa(x)); }
    void update(int x) { T[x].size=T[lson(x)].size+T[rson(x)].size+1; }
    void reverse(int x) {
        if(!x) return;
        swap(lson(x), rson(x)); T[x].reverse^=1;
    }
    void downFlag(int x) {
        if(!is_root(x)) downFlag(fa(x));
        if(T[x].reverse){
            reverse(lson(x)); reverse(rson(x));
            T[x].reverse=0;
        }
    }
    void rotate(int x) {
        int wh=getson(x), fa=fa(x), gfa=fa(fa);
        if(!is_root(fa)) T[gfa].ch[getson(fa)]=x; fa(x)=gfa;
        T[fa].ch[wh]=T[x].ch[wh^1]; T[T[fa].ch[wh]].fa=fa;
        T[x].ch[wh^1]=fa; fa(fa)=x;
        update(fa); update(x); 
    }
    void splay(int x) {
        downFlag(x);
        for(; !is_root(x); rotate(x))
            if(!is_root(fa(x)))
                rotate((getson(fa(x))==getson(x))?fa(x):x);
    }

    void access(int x) {
        for(int y=0; x; y=x, x=fa(x)){
            splay(x); rson(x)=y; update(x);
        }
    }
    void mroot(int x) {
        access(x); splay(x); reverse(x);
    }
    void link(int x, int y) {
        mroot(x); fa(x)=y;
    }
    void cut(int x, int y) {
        mroot(x); access(y); splay(y);
        lson(y)=fa(x)=0;
    }
    int Ask(int x) {
        mroot(t); access(x); splay(x);
        return T[x].size;
    }

    int main()
    {
        freopen(File(in),"r",stdin);
        freopen(File(out),"w",stdout);

    //  ios::sync_with_stdio(false);

        scanf("%d",&n);
        t=n+1;//添加虚拟根,方便查询
        For(i,1,n) scanf("%d",&k[i]);
        For(i,1,n){
            if(i+k[i]<=n) T[i].fa=i+k[i];
            else T[i].fa=t;
        }

        scanf("%d",&m);
        while(m--){
            int x,y,opt;
            scanf("%d%d",&opt,&x);
            x++;//注意下标
            if(opt==1){
                printf("%d\n",Ask(x)-1);//减去虚拟根
            }
            if(opt==2){
                scanf("%d",&y);
                if(x+k[x]<=n) cut(x,x+k[x]);
                else cut(x,t);
                k[x]=y;
                if(x+k[x]<=n) link(x,x+k[x]);
                else link(x,t);
            }
        }

        return 0;
    }

希望各位DaLao指点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值