Link-Cut Tree(LCT)&TopTree讲解

前言:

  Link-Cut Tree简称LCT是解决动态树问题的一种数据结构,可以说是我见过功能最强大的一种树上数据结构了。在此与大家分享一下LCT的学习笔记。提示:前置知识点需要树链剖分和splay。

引例:

  在讲LCT之前先来看一道题:给一棵树,每个点有一个点权,多次操作,操作包含1、修改路径上点权2、查询路径上点权和。这道题显然用树链剖分+线段树就能做,但现在再加两个操作:3、删除树上的一条边4、连接两个点,保证连接后的联通块是一棵树。树的形态发生了改变,树链剖分+线段树这种静态数据结构显然做不了,我们要应用一些动态的数据结构来维护树上信息——平衡树(因为是区间操作所以采用splay,当然也可以用非旋转treap,不过比较麻烦),那么能否沿用树链剖分来解决呢?答案是可以的,但并不是上述的树链剖分方法而是沿用树链剖分的思想。因此可以将LCT看做是树链剖分+splay。

LCT的构建:

  对于一棵树上的一个点,我们依旧选出它的一个子节点作为它的重儿子(这里的重儿子不是根据子树大小决定的),每个点与重儿子之间的边为重边,重边连成的链为重链。因为是动态树,所以重儿子是可变的。对于每一条重链,我们用一棵splay来维护链的信息,splay的key值为每个点的深度。每棵splay的根节点指向这棵splay维护的链的链头的父节点,这里注意是单方向指向,splay根节点能找到指向的链头父节点,但从这个父节点找不到这棵splay的根节点。通俗点说就是父亲不认儿子,儿子认父亲。我们将这些splay组成的树成为辅助树。总的来说,对于原树的一个节点:和它的重儿子在同一棵splay中,被它的轻儿子所在splay的根节点指向。注意辅助树与原树的结构并不相同。如下图所示,无向边是splay中的边,有向边是上述说的儿子认父亲的指向边。

LCT的基本操作:

 先声明一下变量:s[x][0/1]代表x的左右子节点,f[x]表示x的父节点,st[]代表splay时用到的栈,r[x]代表旋转标记

is_root

用途:判断一个点是否是它所在的splay的根。

实现:因为splay的根与其父亲之间是单向指向的边,所以只要判断它的父节点的左右子节点都不是它就好了。

补充:LCT中有许多splay,因此判断splay根的操作与通常splay略有不同。

int is_root(int rt)
{
    return s[f[rt]][0]!=rt&&s[f[rt]][1]!=rt;
}
splay

用途:将一个点旋到它所在splay的根。

实现:先将这个点到splay的根路径上的点都记录下来,从上往下下传标记后按正常splay那样旋到根即可。

补充:因为后续有一个操作需要区间翻转,而在splay之前可能在所旋点到根路径上还存有标记。

void splay(int rt)
{
    int top=0;
    st[++top]=rt;
    for(int i=rt;!is_root(i);i=f[i])
    {
        st[++top]=f[i];
    }
    for(int i=top;i>=1;i--)
    {
        pushdown(st[i]);
    }
    for(int fa;!is_root(rt);rotate(rt))
    {
        if(!is_root(fa=f[rt]))
        {
            rotate(get(rt)==get(fa)?fa:rt);
        }
    }
}
access

用途:将原树中一个点到根的路径变成一条重链并将这个点与它子节点间的重链断开。也就是将这个点到根路径上的所有点放到同一个splay中。

实现:假设要操作点是x,那么x一定是这条到根路径上深度最深的,将x的右子树设为0即切断了与子节点间的链(因为右子树中的点都是深度比他大的),再将x旋到当前splay的根处,然后将x跳到它的父节点(也就是它指向的节点),重复上述操作,但要记录上一次splay的节点,每次splay之后,将当前splay的节点的右儿子设为上次splay的节点。

补充:这是LCT中最重要的操作之一,也是查询路径信息时所必须的一步操作。

void access(int rt)
{
    for(int x=0;rt;x=rt,rt=f[rt])
    {
        splay(rt);
        s[rt][1]=x;
        pushup(rt);
    }
}
reverse

用途:将一个点旋成原树中的根。

实现:假设操作点为x,先将原树中x到根路径变成一条链access(x),再将x旋到它所在splay的根splay(x),这时x没有右儿子,它到原树根路径上的点都在它的左子树中,只要给x打一个旋转标记,这样它就没有了左子树,也就是没有深度比它小的点,它就成为了根。

补充:当询问路径(x,y)上的信息时,通常是先将x旋到原树的根reverse(x),再将y到根(也就是x)路径变成一条链access(y),这时y所在splay中的所有节点就都是原树x到y路径上的节点,只要再把y旋到splay的根O(1)查询即可。

void reverse(int rt)
{
    access(rt);
    splay(rt);
    r[rt]^=1;
}
link

用途:连接两个点。

实现:例如连接x,y两个点,先将x旋到原树的根(因为只有这时x才没有父亲,可以指向),直接将它指向y即可。

补充:连接后只是x单向指向y。

void link(int x,int y)
{
    reverse(x);
    f[x]=y;
}
cut

用途:切断两个点之间的边。

实现:例如切断x,y两个点之间的边,先将x旋到原树的根reverse(x),再将y到原树根的路径变为一条链access(y),然后将y旋到所在splay的根splay(y),因为x,y之间有边,所以x一定是y的左子节点,将x的父亲及y的左儿子置0即可。

补充:有些题不保证x,y之间有边,因此要有一些特判(代码中有实现)。

void cut(int x,int y)
{
    reverse(x);
    access(y);
    splay(y);
    if(s[x][1]||f[x]!=y)
    {
        return ;
    }
    s[y][0]=f[x]=0;
}
find

用途:找到一个点所在原树的根节点。

实现:假设查找点为x,先将x到根路径变成一条链access(x),再将x旋到splay的根splay(x),这时因为根节点深度最小,所以根在x所在splay中最左子树中,直接一直找左子树,直到当前点没有左子节点为止,此时的点就是根。

补充:这个操作通常用于判断两个点的连通性。

int find(int rt)
{
    access(rt);
    splay(rt);
    while(s[rt][0])
    {
        rt=s[rt][0];
    }
    return rt;
}

LCT的时间复杂度:

观察上述几个操作发现除了access之外易证其他操作单次都是均摊O(logn)。那么探究LCT的时间复杂度就在于探究access操作的时间复杂度。因为一次access的路径上指向的边有logn条,所以也就有logn次splay操作,那么这些splay操作是均摊logn的。具体证明参考杨哲的论文《QTREE 解法的一些研究》。

 LCT维护原树子树信息:

 上面只讲了LCT维护原树路径信息,那么LCT能否维护原树子树信息呢?答案是可以的。我们定义一个点的左右子节点为实儿子,指向它的点是它的虚儿子。那么原树路径信息就是实儿子子树信息之和,而原树子树信息其实就是实儿子和虚儿子子树信息之和。那么我们每个点维护两个信息,一个是总儿子信息,也就是原树中子树信息,一个是虚儿子的信息,上传直接像上述那样合并就好了。但能发现虚儿子信息不是一直不变的,观察在哪里改变了虚儿子信息。一个是在access时,另一个是在link时,access时每次往上爬都会将原来的右儿子变成虚儿子,将上次splay的点变成新的右儿子,这里要更新虚儿子信息,当然不管怎样他们都是这个点的儿子,因此总儿子信息不变。link时会把x指向y,y会多一个虚儿子,因此要更新y的虚儿子信息。这里注意严格意义上一个点在原树上的子树信息只包含虚儿子子树信息及实儿子中右儿子的子树信息(因为左儿子子树信息是这个点所在重链中比它深度浅的点的信息和),但因为我们每次查询时都将查询点变为原树的根,所以这个点在LCT上不存在左儿子,因此可以像上述那样维护信息。

以维护原树子树节点数为例,其中sum代表总儿子信息,size代表虚儿子信息。

access
void access(int rt)
{
    for(int x=0;rt;x=rt,rt=f[rt])
    {
        splay(rt);
        size[rt]+=sum[s[rt][1]]-sum[x];
        s[rt][1]=x;
        pushup(rt);
    }
}
link
void link(int x,int y)
{
    reverse(x);
    reverse(y);
    f[x]=y;
    size[y]+=sum[x];
    pushup(y);
}

LCT维护原树边上信息:

通过上述讲解可以发现LCT上的边并不是原树上的边,那么如果题目要求维护原树边上信息该怎么做呢?我们将原树上的边在LCT上也建立一个点来维护这条边的信息,例如:原树上有一条边为(x,y),我们新建一个点z来维护这条边的信息,当原树(x,y)这条边被连接上时,原本在LCT上应该link(x,y),现在改为link(x,z)和link(z,y),同样在删边时也要cut(x,z)和cut(z,y)。因为有删边操作,所以要记录原树每条边的两个端点。

TopTree:

上面说到了如何维护原树子树信息即维护LCT上轻儿子信息,那么如何修改原树的子树信息呢?因为一个点的轻儿子数量是不固定的,如果只是单纯的记录每个点的轻儿子并打标记下传的话,那么就无法保证下传的时间复杂度,所以我们引入了一种新的数据结构——TopTree。TopTree就是对于LCT上每个点,建立一个splay(即新建一些点组成一个splay),将splay的根作为这个点的轻儿子,而这个点原先所有的轻儿子则按一定顺序连到splay的每个节点下面作为他们的轻儿子(轻儿子顺序因题而异,且轻儿子顺序决定了新建splay的key值),TopTree的结构如图所示。

其中带箭头指向的是轻儿子,左边是LCT,右边是TopTree。1~6号节点为原树点,7、8号节点为用来管理轻儿子的建立的splay上的点。可以发现整棵TopTree分为两部分,维护一条重链的splay即原LCT上的splay和维护一个点轻儿子的splay即新建的splay。这样在维护轻儿子的splay上移动即可实现在一个点的不同轻儿子间移动,同样对于原树子树修改也可以通过下传到维护轻儿子的splay中再进一步下传到对应轻儿子所在重链的splay中来实现。因为LCT中的splay和TopTree中新建的splay作用不同,所以对于splay的所有操作都要在两种splay中分别实现即写两种splay的操作,代码量巨大且及其难调。因为每个点只有被当作轻儿子时才会在上面新建一个节点来维护他的信息,而每个点被当作轻儿子一次,所以新建节点数为$O(n)$。

LCT的练习题:

BZOJ2049[Sdoi2008]洞穴勘探(LCT模板题,只有link和cut)

BZOJ3282Tree(LCT模板题,单点修改,求路径异或和)

BZOJ2631tree(LCT模板题,路径加,路径乘,求路径点权和)

BZOJ2002[Hnoi2002]弹飞绵羊(LCT练习题,重点在于如何转化成LCT)

BZOJ3669[Noi2014]魔法森林(LCT经典题,利用LCT解决二维最小生成树)

BZOJ4530[Bjoi2014]大融合(LCT维护子树信息)

BZOJ3091城市旅行(LCT区间信息合并)

转载于:https://www.cnblogs.com/Khada-Jhin/p/9743397.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值