LCT——Link Cut Tree及其应用

我们知道,想要维护树上的一段区间,我们可以采用重链剖分来将其划分

但是,树链剖分只能够维护静态(树的形态不发生变化)的树,倘若我们需要动态对树的形态进行修改,比如将某个结点换为树的根、树中边的增删、子树合并和分离操作等,并需要在线地回答相关询问,那么每一次修改后,轻重链都需要重构,效率就会大大降低

所以,我们需要一种数据结构能够动态地维护树上的区间

这就是今天要用到的LCT

LCT是在1982年由Tarjan大佬等人提出的

LCT原理:

前提概念:

splay树:

一种通过splay操作来维护的平衡树

splay(u)可以将u结点旋转成为这棵平衡树的根

实链:

对于结点u而言,我们任意地选取一个儿子v,那么连接u与v的边就称作实边

实边的特点是儿子能够访问父亲,父亲也能够访问儿子,即:tr[u].s[1]或tr[u].s[0]==v且tr[v].p==u

全部由实边构成的链叫做实链

虚链:

对于结点u而言,除了一条实边外,其他连接儿子的边均为虚边

虚边的特点是儿子能够访问父亲,父亲不能够访问儿子,即:tr[v].p==u且tr[u].s[1]!=v&&tr[u].s[0]!=v

实现原理:

为了配合树的形态变化,我们定义LCT作为辅助树,这棵树是怎么得到的呢?

我们对原树进行虚实链剖分,且令每一条实链都成为一颗splay树

这样,所有splay树就由虚边连接在一起,构成一个森林

性质:

1.每棵splay树都维护着一条按原树深度严格递增的实链(不会出现深度相同的节点)

2.每个结点都被包含,且仅被包含在一棵splay树中

3.虚边用伸展树的根来维护

4.LCT的实链所对应的伸展树是动态变化的,虚实边也是可以动态变化的,虚边与实边动态转化

5.无论如何虚实变化旋转,所有节点的相对位置都不变,如果原树路径(x,y)中没有z节点,那么操作完后,路径(x,y)也不会出现z结点

LCT的操作函数:

这里要区分splay树的根和原树的根

LCT总共有7种基本操作:

access(x)——在x节点到原树根之间打通一条实链

makeroot(x)——把x结点变为原树的根节点

findroot(x)——找到x所在的原树的根节点,并把原树的根节点旋转成他所在的splay树的根

split(x,y)——在x到y之间的路径建立一棵splay树,这棵树的根节点为y

link(x,y)——如果x和y之间不连通,加入一条边连接x和y

cut(x,y)——如果x和y连通,剪断这条边

isroot(x)——判断是否为所在的splay树的根

实现代码如下:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1E5 + 10;

int n, m;
struct node{
    int s[2], p, v;
    int sum;//本次的sum是异或和
    int tag;//翻转懒标记
} tr[N];
int stk[N];

//翻转左右子树
void reverse(int x){
    swap(tr[x].s[0], tr[x].s[1]);
    tr[x].tag ^= 1;
}

//由下往上更新结点信息
void pushup(int x){
    tr[x].sum = tr[tr[x].s[0]].sum ^ tr[tr[x].s[1]].sum ^ tr[x].v;
}

//由上往下传递懒标记翻转节点
void pushdown(int x){
    if(tr[x].tag){
        reverse(tr[x].s[0]);
        reverse(tr[x].s[1]);
        tr[x].tag = 0;
    }
}

//splay树根节点的特点:
//与父亲为虚边连接
//如果父亲的左右儿子都不是自己
//说明连接x与父亲的边为虚边
//说明x是splay树的根节点
bool isroot(int x){
    return tr[tr[x].p].s[0] != x && tr[tr[x].p].s[1] != x;
}

//比起普通的rotate,多了一个判断是否为splay树的根的操作
//因为需要确定更新父子关系的方式
void rotate(int x){
    int y = tr[x].p;
    int z = tr[y].p;
    int k = tr[y].s[0] == x;

    //把x转到y的位置
    if(!isroot(y)) tr[z].s[tr[z].s[1] == y] = x;//如果y不是splay的根
    tr[x].p = z;                                //那么z与x用实边连接
                                                //否则z与x用虚边连接
    //把x的异儿子转到x的位置
    tr[y].s[k ^ 1] = tr[x].s[k];
    tr[tr[x].s[k]].p = y;

    //把y转到x的异儿子的位置
    tr[x].s[k] = y;
    tr[y].p = x;

    //更新节点信息
    pushup(x);
    pushup(y);
}

//把x旋转成splay树的根
void splay(int x){
    //在splay之前要把旋转会经过的路径上的点的tag全部下放
    int top = 0, r = x;
    stk[++top] = r;
    //将到根节点的路径上的点全部进栈
    while(!isroot(r)) stk[++top] = r = tr[r].p;
    //出栈pushdown
    while (top) pushdown(stk[top--]);
    while(!isroot(x)){
        int y = tr[x].p, z = tr[y].p;
        //当y不是splay树的根的时候
        //做双旋
        //等价于普通splay树的z!=k
        if(!isroot(y)){
            //直转中,折转底
            (tr[y].s[0] == x) ^ (tr[z].s[0] == y) ? rotate(x) : rotate(y);
        }
        rotate(x);
        //否则做单旋
    }
}


//打通一条x到原树根的实链
//同时将x转成splay树的根
void access(int x){
    //留存x编号
    int z = x;
    //y为上一棵splay树的根节点,显然初始化为0并不影响
    //x为当前一棵splay树的根节点

    // 主要过程:
    // splay树的中序遍历就是结点按深度由小到大的排序
    // 由于我们需要打通一条x到原树的实链
    //那么上一棵splay树y的深度一定都比x的深度要大
    //所以我们将其接到x树的根节点的右子树上
    //然后原本x连接右子树的边就要变成虚边,也就是断开父可以访问子的这条边
    //然后x再通过虚边跳到他的父亲上面去
    //这样不断迭代,直到迭代到x跳到含有原树根的splay树上
    for (int y = 0; x; y = x,x=tr[x].p){
        //每次循环,先把x旋转成它锁在的splay树的根节点
        //然后把y接到x的右子树上
        //再更新x
        //然后x跳到x的父亲那里(通过虚边)
        //y变成x
        splay(x);
        tr[x].s[1] = y;
        pushup(x);
    }
    //经过这样连接,splay树会变成一条链,我们再旋转x,减少树高
    splay(z);
}

//把x变为原树根
void makeroot(int x){
    //首先打通一条x到原树根的实链
    access(x);
    //在这条实链中,x是深度最大的节点,原树根是深度最小的节点
    //同时x为这棵splay树的根
    //因此只需要翻转左右子树,把中序遍历顺序颠倒
    //x就变为了深度最小的节点,既是splay树的根,也是原树根
    reverse(x);
}

int findroot(int x){
    //先打通一条x到原树根的实链
    access(x);
    //这个时候x是splay树的根
    //当x的左子树存在时
    //先下放懒标记
    //在跳到左子树
    while(tr[x].s[0]){
        pushdown(x);
        x = tr[x].s[0];
    }
    //跳到最后左子树不存在时,
    //就是跳到了中序遍历第一个,也就是深度最小的结点,也就是原树根节点
    //把原树根转成splay树的根
    splay(x);
    return x;
}

//把路径(x,y)创建为一棵splay树
void split(int x,int y){
    //先把x变成原树根
    makeroot(x);
    //然后打通y与原树根的实链,也就是打通y到x的实链
    //并把y转成splay树的根
    access(y);
}

//如果x不连通,就加一条边连接x与y
void link(int x,int y){
    //先把x变成原树根
    makeroot(x);
    //如果y的原树根不是x,说明x与y不连通
    //把y连成x的父亲
    //用虚边连接
    //这样不用改动其他任何边
    if(findroot(y)!=x){
        tr[x].p = y;
    }
}

void cut(int x,int y){
    //先把x变为原树根
    makeroot(x);
    //如果x是y的原树根
    //说明x和y经过若干个点连通
    //如果y的父亲是x且y没有左子树
    //说明x和y的深度差为1,x是y的前驱,y是x的后继
    if(findroot(y)==x&&tr[y].p==x&&!tr[y].s[0]){
        tr[x].s[1] = tr[y].p = 0;
        pushup(x);
    }
}

int main(){
    cin >> n >> m;
    for (int i = 1; i <= n;i++){
        cin >> tr[i].v;
    }
    int t, x, y;
    while(m--){
        cin >> t >> x >> y;
        if(t==0){
            split(x, y);
            cout << tr[y].sum << endl;
        }
        else if(t==1){
            link(x, y);
        }
        else if(t==2){
            cut(x, y);
        }
        else{
            splay(x);
            tr[x].v = y;
            pushup(x);
        }
    }
    return 0;
}

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值