Link Cut Tree详解

==Warning:千万不要跳读==
参考博客:https://www.cnblogs.com/flashhu/p/8324551.html

什么是动态树?

动态树问题, 简称LCT,近几年在OI中兴起的一种新型问题,是一类要求维护一个有根树森林,支持对树的分割, 合并等操作的问题。由RobertE.Tarjan为首的科学家们提出。

LCT的功能

LCT:
- 分裂成两棵树
- 查询一个节点所在树的根
- 合并两棵树
- 求一条路径权值信息(求和/最值)
- 更改单点的权值
- 更改一棵树根节点
- 维护连通性
- 各种神奇操作

LCT的主体思想

预备知识:树链剖分 & Splay

如何操作呢?

首先我们拿到一颗树。
先将其剖一剖,然后处理出轻重链,如图:
EFvwH.png
好了,因为我们轻重边的条数是 O(logn) O ( l o g n ) 级别的。
Q:那么能不能更灵活地使用轻重剖分呢?
A:我们可以发现,以前我们都是用线段树来维护轻重链的,那么能不能用更加灵活的平衡树维护呢?
Q:这样的话每条重链没法维护啊!
A:如果重儿子没有size大小的限制,也不需要求top呢?
Q:这样的话,平衡树就可以派上用场了!可以对每一条重链单独建一棵平衡树!
A:恩,那么这样你维护形态就不太方便了,有没有更好的方法呢?
Q:可以建边将这些Splay连起来啊!
A:%%%%%
这样我们的整棵树就非常灵活了。
这就是LCT。

LCT的性质

  • 由于每一个子树的根节点不会有两条向子树去的重边,所以这也就意味着每棵Splay中的点不会有在LCT中深度相同的,同时这是一条链,所以可知它们的深度在中序遍历中是递增的。
  • 每个节点包含且仅包含在一个Splay中
  • 置于为什么我们不用按照传统的轻重剖分去剖,因为总的期望是$O(log_n)$

基本的操作

access(x)

这个操作是LCT最基本的操作,请读者弄懂它的功能。
它的功能就是将x到x所在LCT的根的所有边都变成重边。
那么如果一个节点有两条重边连向儿子节点了怎么办?
我们显然是要分离出来一个的。
怎么断开大家可以先想一想,在讲完具体过程会进行说明。

具体过程

EFZsS.png
假定这棵树是上图的形态,我们要更改9到1的边,将它们都改为重边。
我们先将9旋至该重链Splay的根,然后将多余的部分改成轻边(也就是分离Splay),会变成下图:
EHFtS.png

这时,我们将9的父亲6旋至其所在重链(Splay)的根,然后将6和9这两棵Splay并起来,但是这样Splay中就有两个深度一样的节点了,为了保证正确性,我们还是需要把与当前9这条边矛盾的边删掉,也就是再做分裂。

EHzRu.png

那么这时,很显然边6-8与边6-9都将是重边了,可是6不能出去两个重边(注意这里虽然6画在上面,可1还是根),我们显然需要将6-8变成轻边。
EHQSO.png
在将要连接的6-9连上,这样我们就大功告成了,可以看到,1-9的路径上都是重边。
我们再来看一看代码:

for(int y=0;x;y=x,x=fa[x])
    Splay(x),/*变重为轻*/,update(x);

那么我们到底边那条边为轻,并且我们的分裂在哪里呢?
前面说过,由于我们的Splay是用边相连的,这些边有一些技巧就是“认父不认子”,这也就使得替换和分离操作格外好做,我们只需要将需要断的地方的父亲的儿子改成另一个编号就可以了。
那么又回到了我们刚刚的问题:我们到底要变哪一个呢?
相信你有正确的答案了,我们将x旋转至重链splay的根后,只需要替换掉其右子树变为y就可以了。(这里的y指的是上一个x)。
为什么呢?显然根据性质2,Splay中序遍历的深度递增,那么我们既然要将深度假设为p的转到重链的根上去,并且要添加一个深度为p+1的进来,那么显然比p大的都是不合法的,这一部分也就是p为根后的p的右子树。

for(int y=0;x;y=x;x=fa[x])
    Splay(x),rc=y,update(x);

至此,我们完美的介绍完了access操作。
不用担心,接下来的操作都基于它,并且都非常简单,就像Splay的Splay操作要占据大量篇幅一样。

chroot(x)

chroot(x):将根换为节点x。
当然,怎么换根呢?
我们不能再将整棵树套一层splay,那么可以想一想之前的access操作有没有什么帮助。
首先,将x换到根并不难,不好处理的是从原来的根到x的这些点。
等等!这些点不就是access(x)更改的这棵splay中的点么?
而且除了这些点之外的其它点都没有动。
所以我们只需要做一遍access(x),然后将x点splay到所在重链的根上,这样就成功将x移到了根上。
可以发现,剩下点的顺序只是相当于反过来了,所以我们打一个splay的区间翻转标记就可以了。

inline void Colorf(int x){if(!x) return ;swap(rc,lc); col[x]^=1;}
inline void chroot(int x){access(x); Splay(x); Colorf(x);}

findroot(x)

findroot(x):找x所在LCT的根。
将x换到重链splay的根上去,然后由于LCT的性质,所以一直向它的左子树找,找到最后一个就可以。
注意在过程中需要pushdown我们的chroot的标记。
最后伸展一下保证复杂度。

inline int findroot(int x){
    access(x); Splay(x); 
    while(lc) pushdown(x),x=lc; 
    return x;
}

split(x,y)

split(x,y):使x到y这条路径可以直接访问。
可以先自己想一想。
其实就是将x放到根上面去,然后将y到根都换为重边,然后为了保证总体复杂度,要记得将y splay一下。
这样,我们最后只需要访问y节点来获得信息。
这也是LCT比较温和的一点——基于树剖而又不需要找LCA去更新,直接用基本操作就可以解决,代码方便快捷。

inline void split(int x,int y){chroot(x); access(y); Splay(y);}

link(x,y)

link(x,y):将x和y所在的LCT合并起来,也就相当于并查集里面的Merge操作。
当然,也是向并查集那样,直接判断一下它们是否在同一个LCT中就可以了:

inline void link(int x,int y){
    chroot(x); 
    if(findroot(y)!=x) tr[x].fa=y;
}

根据代码可以看出,这里连的是一条轻边,因为连接重边可能会破坏性质2

cut(x,y)

cut(x,y):将(x,y)这条边断开。
当这条边合法时,显然直接断开就可以了

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

需要注意的是,这里写的比较隐晦,在split操作完成后y是这条重链的根,而不是x。
那么如果可以不合法呢?
我们先想到它们必须在一棵LCT中。
因为access(y)以后,假如y与x在同一Splay中而没有直接连边,那么这条路径上就一定会有其它点,在中序遍历序列中的位置会介于x与y之间。

inline void cut(int x,int y){
    chroot(x); 
    if(findroot(y)==x&&tr[x].fa==y&&!rc)
        tr[x].fa=tr[y].ch[0]=0,update(y);
}

这里的细节就更不显眼了,因为findroot(y)中将y又旋至了根,所以在判断完findroot(y)==x后y就是重链的根节点了,而不是x。


至此,常用操作已经基本结束。
模板题:【Luogu】Link-Cut-Tree

#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
#include<algorithm>

using namespace std;
#define LL long long

inline int read(){
    int x=0,f=1;char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x*f;
}
#define rc tr[x].ch[1]
#define lc tr[x].ch[0]
int N,M;

const int MAXN = 2100000;
const int INF = 2147483600;

struct data{
    int fa,ch[2];
    int sum,val;
}tr[MAXN+1];
inline void update(int x){tr[x].sum=tr[lc].sum^tr[rc].sum^tr[x].val;}
inline void Union(int son,int fa,int c) {tr[fa].ch[c]=son; tr[son].fa=fa;}
inline int chd(int x){return tr[tr[x].fa].ch[0]==x?0:1;}
inline bool nroot(int x){return tr[tr[x].fa].ch[0]==x||tr[tr[x].fa].ch[1]==x;}
inline void rotate(int x){
    //cout<<x<<endl;
    int fa=tr[x].fa; int fafa=tr[fa].fa; 
    int cx=chd(x),cfa=chd(fa),B=tr[x].ch[cx^1];
    //Union(tr[x].ch[cx^1],fa,cx);  
    if(nroot(fa)) tr[fafa].ch[cfa]=x; tr[x].fa=fafa; Union(fa,x,cx^1);
    if(B) tr[B].fa=fa; tr[fa].ch[cx]=B;
    update(fa); return ;
}
/*inline void rotate(int x){
    int y=tr[x].fa,z=tr[y].fa,k=tr[y].ch[1]==x,w=tr[x].ch[!k];
    if(nroot(y)) tr[z].ch[tr[z].ch[1]==y]=x; tr[x].ch[!k]=y; tr[y].ch[k]=w;
    if(w) tr[w].fa=y; tr[y].fa=x; tr[x].fa=z;
    update(y); return ; 
}*/
bool col[MAXN+1];
inline void Colorf(int x){if(!x) return ;swap(rc,lc); col[x]^=1;}
inline void pushdown(int x){if(col[x]){Colorf(lc); Colorf(rc);col[x]=0;}}
int st[MAXN+1];
void Splay(int x){
    int top=0,y=x; st[++top]=y; while(nroot(y)) st[++top]=y=tr[y].fa; 
    while(top) pushdown(st[top--]); 
    while(nroot(x)){
        //cout<<tr[x].fa<<" "<<x<<" "<<tr[tr[x].fa].ch[0]<<" "<<tr[tr[x].fa].ch[1]<<endl;
        int y=tr[x].fa;
        if(nroot(y)) rotate((chd(x)==0)^(chd(y)==0)?x:y);
        rotate(x);
    } update(x);
    return ;
} 
inline void access(int x){for(int y=0;x;y=x,x=tr[x].fa) Splay(x),rc=y,update(x);}
inline int findroot(int x){access(x); Splay(x); while(lc) pushdown(x),x=lc; return x;}
inline void chroot(int x){access(x); Splay(x); Colorf(x);}
inline void split(int x,int y){chroot(x); access(y); Splay(y);}
inline void link(int x,int y){chroot(x); if(findroot(y)!=x) tr[x].fa=y;}
inline void cut(int x,int y){chroot(x); if(findroot(y)==x&&tr[x].fa==y&&!rc) tr[x].fa=tr[y].ch[0]=0,update(y);}

int main(){
    //freopen(".in","r",stdin);
    //freopen("a.out","w",stdout);
    N=read(),M=read();
    for(int i=1;i<=N;i++) tr[i].val=read();
    while(M--){
        int opr=read(); int x=read(),y=read();
        switch(opr){
            case 0: split(x,y),printf("%d\n",tr[y].sum); break;
            case 1: link(x,y); break;
            case 2: cut(x,y); break;
            case 3: Splay(x); tr[x].val=y; 
        }
    }
    return 0;
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值