lct

转自http://www.cnblogs.com/flashhu/p/8324551.html

LCT(Link-Cut Tree),就是动态树的一种,用来维护一片森林的信息,滋磁的操作可多啦!

    动态连边、删边
    合并两棵树、分离一棵树(跟上面不是一毛一样吗)
    动态维护连通性
    维护每一条路径上的信息(最值,总和等)
    所有平衡树滋磁的操作(其主体就是一大堆Splay平衡树)
    更多意想不到的操作。。。。。。

它对树的处理非常精妙,主要思想其实跟树链剖分差不多(建议先学树剖,本蒟蒻因为考试天天考LCT就先来学这个了)
与树剖的区别就在于树剖是静态的,把整个树的边分成重边和轻边,若干重边连在一起用线段树等静态数据结构维护链上信息。
而LCT使用了更加灵活的数据结构Splay来维护每一条链。当然这些链是时时刻刻变化着的,一下子被拉起来,一下又断掉了。
于是就可以动态连边了,整棵树也就动起来了。

想学Splay的话,推荐巨佬yyb的博客

LCT的主要性质如下:

    每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增。
    是不是有点抽象哈
    比如有一棵树,根节点为1

(深度1),有两个儿子2,3(深度2),那么Splay有3种构成方式:
{12},{3}
{13},{2}
{1},{2},{3}(每个集合表示一个Splay)
而不能把1,2,3

    同放在一个Splay中(存在深度相同的点)

    每个节点包含且仅包含于一个Splay中

    边分为轻边和重边,重边包含在Splay中,而轻边总是由一棵Splay指向另一棵Splay。
    因为性质2,当某点在原树中有多个儿子时,只能向其中一个儿子拉一条路径(只认一个儿子),而其它儿子是不能在这个Splay中的。
    那么为了保持树的形状,我们要让到其它儿子的边变为轻边,由对应儿子所属的Splay的根节点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。

各种操作
access(x)

LCT核心操作,其它所有的操作都是在此基础上完成的。
因为性质3,我们不能总是保证两个点之间的路径是直接连通的(在一条Splay上)。
access即定义为打通根节点到指定节点的路径。
蒟蒻深知没图的痛苦qwq
所以还是来几张图吧。
下面的图片参考YangZhe的论文
有一棵树,假设一开始轻边和重边是这样划分的(虚线为轻边)

那么所构成的LCT可能会长这样(绿框中为一个Splay,可能不会长这样,但只要满足中序遍历按深度递增(性质1)就对结果无影响)

现在我们要access(N)
,把A−N的路径拉起来变成一条Splay。
因为性质2,该路径上其它链都要给这条链让路,也就是把每个点到该路径以外的重边变轻。
所以我们希望轻重边重新划分成这样。

然后怎么实现呢?
我们要一步步往上拉。
首先把splay(N),使之成为当前Splay中的根。
为了满足性质2,原来N−O的重边要变轻。
因为按深度O在N的下面,在Splay中O在N的右子树中,所以直接单方面将N的右儿子置为0

(认父不认子)
然后就变成了这样——

我们接着把N
所属Splay的轻边指向的I(在原树上是L的父亲)也转到它所属Splay的根,splay(I)。
原来在I下方的重边I−K要变轻(同样是将右儿子去掉)。
这时候I−L就可以变重了。因为L肯定是在I下方的(刚才L所属Splay指向了I),所以I的右儿子置为N

,满足性质1。
然后就变成了这样——

I
指向H,接着splay(H),H的右儿子置为I

。

H
指向A,接着splay(A),A的右儿子置为H

。

A−N

的路径已经在一个Splay中了,大功告成!
代码其实很简单。。。。。。循环处理,只有四步——

    转到根;
    换儿子;
    更新信息;
    当前操作点切换为轻边所指的父亲,转1

inline void access(int x){
    for(int y=0;x;y=x,x=f[x])
        splay(x),c[x][1]=y,pushup(x);//儿子变了,需要及时上传信息
}

makeroot(x)

只是把根到某个节点的路径拉起来并不能满足我们的需要。更多时候,我们要获取指定两个节点之间的路径信息。
然而一定会出现路径不能满足按深度严格递增的要求的情况。根据性质1,这样的路径不能在一个Splay中。
Then what can we do?
makeroot

定义为换根,让指定点成为原树的根。
代码

inline void pushr(int x){//Splay区间翻转操作
    swap(c[x][0],c[x][1]);
    r[x]^=1;//r为区间翻转懒标记数组
}
inline void makeroot(int x){
    access(x);splay(x);
    pushr(x);
}

还是很简单有木有
解释一下,access(x)
后x在Splay中一定是深度最大的点对吧。
splay(x)后,x在Splay中将没有右子树(性质1)。于是翻转整个Splay,使得所有点的深度都倒过来了,x

没了左子树,反倒成了深度最小的点(根节点),达到了我们的目的。
findroot(x)

找x
所在原树的树根,主要用来判断两点之间的连通性(findroot(x)==findroot(y)表明x,y

在同一棵树中)
代码:

inline int findroot(R x){
    access(x); splay(x);
    while(c[x][0])pushdown(x),x=c[x][0];
//如要获得正确的原树树根,一定pushdown!详见下方update
    splay(x);//伸展一下,Splay的特性,保证复杂度(好像牵涉到玄学的势能分析,蒟蒻什么也不会啊QvQ)
    return x;
}

同样利用性质1,不停找左儿子,因为其深度一定比当前点深度小。
Important update

蒟蒻真的一时没注意这个问题。。。。。。Splay根本没学好
找根的时候,当然不能保证Splay中到根的路径上的翻转标记全放掉。
所以最好把pushdown写上。
Candy巨佬的总结对pushdown问题有详细的分析
只不过蒟蒻后来经常习惯这样判连通性(我也不知道怎么养成的)

makeroot(x);
if(findroot(y)==x)//后续省略

这样好像没出过问题,那应该可以证明是没问题的(makeroot保证了x在LCT的顶端,access(y)+splay(y)以后,假如x,y在一个Splay里,那x到y的路径一定全部放完了标记)
导致很久没有发现错误。。。。。。
另外提一下,假如LCT题目在维护连通性的情况中只可能出现合并而不会出现分离的话,其实可以用并查集哦!(实践证明findroot很慢)
这样的例子有不少,比如下面“维护链上的边权信息”部分的两道题都是的。
甚至听到Julao们说有少量题目还专门卡这个细节。。。。。。XZY巨佬的博客就提到了(我太弱啦,暂时并不会)
split(x,y)

神奇的makeroot
已经出现,我们终于可以访问指定的一条在原树中的链啦!
split(x,y)定义为拉出x−y的路径成为一个Splay(本蒟蒻以y

作为该Splay的根)
代码

inline void split(int x,int y){
    makeroot(x);
    access(y);splay(y);
}

x成为了根,那么x到y的路径就可以用access(y)
直接拉出来了,将y转到Splay根后,我们就可以直接通过访问y

来获取该路径的有关信息
link(x,y)

连一条x−y
的边(本蒟蒻使x的父亲指向y

,连一条轻边)
代码

inline bool link(int x,int y){
    makeroot(x);
    if(findroot(y)==x)return 0;//两点已经在同一子树中,再连边不合法
    f[x]=y;
    return 1;
}

如果题目保证连边合法,代码就可以更简单

inline void link(int x,int y){
    makeroot(x);
    f[x]=y;
}

cut(x,y)

将x−y
的边断开。
如果题目保证断边合法,倒是很方便。
使x为根后,y的父亲一定指向x,深度相差一定是1。当access(y),splay(y)以后,x一定是y

的左儿子,直接双向断开连接

inline void cut(int x,int y){
    split(x,y);
    f[x]=c[y][0]=0;
    pushup(y);//少了个儿子,也要上传一下
}

那如果不一定存在该边呢?
充分利用好Splay和LCT的各种基本性质吧!
先判一下连通性,再看看x,y
是否有父子关系,还要看x是否有右儿子。
因为access(y)以后,假如y与x在同一Splay中而没有直接连边,那么这条路径上就一定会有其它点,在中序遍历序列中的位置会介于x与y之间。
那么可能x的父亲就不是y了。
也可能x的父亲还是y,那么其它的点就在x

的右子树中。
只有三个条件都满足,才可以断掉。

inline bool cut(int x,int y){
    makeroot(x);
    if(findroot(y)!=x||f[x]!=y||!c[x][1])return 0;
    f[x]=c[y][0]=0;
    pushup(y);
    return 1;
}

如果维护了size

,还可以换一种判断

inline bool cut(int x,int y){
    makeroot(x);
    if(findroot(y)!=x&&sz[y]>2)return 0;
    f[x]=c[y][0]=0;
    pushup(y);
}

解释一下,如果他们有直接连边的话,access(y)
以后,为了满足性质1,该Splay只会剩下x,y两个点了。
反过来说,如果有其它的点,size不就大于2

了么?

其实,还有一些LCT中的Splay的操作,跟我们以往学习的纯Splay的某些操作细节不甚相同。
包括splay(x),rotate(x),nroot(x)
(看到许多版本LCT写的是isroot(x)

,但我觉得反过来会方便些)
这些区别之处详见下面的模板题注释。
题目总览
模板1

洛谷P3690 【模板】Link Cut Tree (动态树)(点击进入题目)
最基本的LCT操作都在这里,也没有更多额外的复杂操作了,确实很模板。

#include<cstdio>
#include<cstdlib>
#define R register int
#define I inline void
#define lc c[x][0]
#define rc c[x][1]
#define G ch=getchar()
#define in(z) G;\
    while(ch<'-')G;\
    z=ch&15;G;\
    while(ch>'-')z*=10,z+=ch&15,G;
const int N=300009;
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 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);
}
inline int findroot(R x){//找根(在真实的树中的)
    access(x);splay(x);
    while(lc)pushdown(x),x=lc;
    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[x]==y&&!rc)){
        f[x]=c[y][0]=0;
        pushup(y);
    }
}
int main()
{
    register char ch;
    R n,m,i,type,x,y;
    in(n);in(m);
    for(i=1;i<=n;++i){in(v[i]);}
    while(m--){
        in(type);in(x);in(y);
        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;
}

模板2

洛谷P1501 [国家集训队]Tree II(点击进入题目)
感觉难度评定也高了点吧
有了链上乘法和链上加法,那肯定是像线段树区间修改那样使用懒标记了。
放标记的做法来自[模板]线段树2
那里发题解的Dalao们都讲得挺好的,本蒟蒻参考一下。
蒟蒻的题解在这里
模板3

洛谷P3203 [HNOI2010]弹飞绵羊(点击进入题目)
略带一点思维,但是代码比模板还简单。。。。。。
在LCT模板基础上进行灵活运用,可以使代码和程序都做到高效。
蒟蒻的题解在这里
操作升级!(模板以外的总结)
维护链上的边权信息

普通LCT维护点权,那对于边权呢?比如要获取一条路径上最长的边,等等。
这种维护的最经典的应用,大概就是动态维护一颗最小/大生成树了(需要获取边长度等信息)。
我起初有一种想法。
一棵树,除了根节点,每个节点有且仅有一条父边。
那么如果我们只要边的信息,可不可以就把LCT中的点的点权当成父边的边权呢?
在其它不少数据结构中好像是可以这样做的。
只不过LCT性质非常特殊。一旦换了根,原先边的父子关系就破坏了。所以并不能这样做。
那又如何是好呢?
我在网上看到有些博客介绍的方法,是把边置于LCT外,然后在LCT节点中维护父边和重子边的编号,需要更新信息时从外部获取,在access,link,cut时额外更改。
这样好像挺麻烦的,要维护那么多东西。
另一种更抽象的做法,也是我要分享的,就是两个字——拆边,把边视作为一个点,向该边的两个端点连两条边。
只需要维护边权的时候,边权存在LCT中代表边的点权里,因为不需要维护真正的点权,所以LCT中的代表点的点权可以设成空的(0),不会影响信息的正确性。于是就变成了维护点权。
当然了,原先我们的link和cut是对于两个点的,现在有点不一样了(因为两点之间又多了一个代表边的点),写法就视具体需要来分析啦!
比如,截取了下面例题二的非正常link与cut部分

inline void link(int x){
    int y=e[x].u,z=e[x].v;
    mroot(z);
    f[f[z]=x]=y;
}
inline void cut(int x){
    access(e[x].v);splay(x);
    lc=rc=f[lc]=f[rc]=0;
    pushup(x);
}

当然可以两遍link、cut啦,代码简单,不过有些操作浪费了,牺牲了一点常数。
题目做得还不够,因此写法也不够成熟,还望Dalao们指教。
例题一

洛谷P4180 [Beijing2010组队]次小生成树Tree
其实这道题用LCT不是正解,不过是可做的(update:我后来又写了LCT),放在这里一下吧。
用LCT维护最小生成树,再去枚举非树边尝试替换树边,更新最优答案。
TPLY巨佬也用LCT做的(某问题导致常数巨大?),他的题解在这里
蒟蒻题解在这里
例题二

洛谷P2387 [NOI2014]魔法森林
我太弱啦!这种双关键字的维护我是真的没有思路,边看着题解边打出来的。orz XZY&XZZ两位巨佬,题解点赞!
不过作为LCT维护边权的好题目还是值得分享。
蒟蒻题解在这里
维护虚子树信息总和与原树信息总和

有时候,我们需要的并不是维护链的信息,而是子树的信息。要知道,LCT是长于维护链的信息,而弱于维护子树信息(这方面不得不承认树剖的用处了,还要赶紧学)。然而动态的连边和断边,又不得不要求我们使用LCT来维护。

其实,我们还是不会束手无策的。

维护子树信息总和的一些常见题型,无非就是询问整棵(原树中的)子树的总大小、权值总和之类的东西。
我们已经可以通过辅助树Splay来获知实子树(也就是一条链)的信息总和。
既然原树的边只有轻与重,子树只有实与虚,那么如果要想维护好原树的信息总和,我们是不是首先要知道虚子树的信息总和?
注:以下设虚子树信息总和用数组si表示,原树信息总和用s表示。此处si[x]只包含x所有虚子树(通过轻边指向x)的信息总和,而s[x]实际上是在LCT中的所有儿子的信息总和(包括辅助树Splay中相对的左右儿子的总和与被轻边所指的虚子树的绝对的总和)。
那么如果我们确定了si[x]的值,是不是就知道了s[x]的值了?实加虚嘛!
这就是pushup,代码

inline void pushup(int x){
    s[x]=s[c[x][0]]+s[c[x][1]]+si[x]+1;//大小总和,注意别漏了+1,自己也要算上
    //或者
    s[x]=s[c[x][0]]+s[c[x][1]]+si[x]+v[x];//权值总和,自己同样也要算上
}

先不对这种方法是怎样来的这个问题追本溯源。
还是直接假设我们已经提前维护好了si与s吧。
现在我们只考虑LCT中的每个操作会对这些信息产生怎样的影响。

    splay(x):显然这一操作只会改变节点在辅助树Splay中的相对位置,并不会对树中的信息产生任何影响。
    access(x):这个时候,我们发现,循环中每一次splay(x)后,我们更改了x的右子树,也就是更改了轻重边,那么会对信息产生影响。
    这里的影响,无非就是得到了一个虚儿子,失去了一个虚儿子,总和没有改变。
    于是直接改就好啦,si加上原来右儿子的s,再减去新的右儿子的s。
    与原模板略有不同的代码

inline void access(int x){
    for(int y=0;x;x=f[y=x]){
        splay(x);
        si[x]+=s[c[x][1]];
        si[x]-=s[c[x][1]=y];
        pushup(x);
//如果pushup只是更新原树信息总和s的话,甚至这里可以不写,毕竟加一个减一个,和没变
    }
}

    makeroot(x):并无影响。(其实理论上这一操作使整棵原树的形态都发生了变化,但是这一操作并不直接用来获取信息。用split获取信息时,makeroot(x)后的access(y)+splay(y)会完成信息的更新)
    findroot(x):显然无影响(跳左儿子又没改变树的形态)
    split(x,y):显然无影响(除了调用三个函数外什么都没干)
    link(x,y):这就有影响了。y多了一个儿子x,那么s[y],si[y]都加上s[x]。注意,在加上s[x]前,还要额外地把y转到根(access(y)+splay(y)),再加,不然会影响信息的正确性。
    与原模板略有不同的代码

//保证连边合法
inline void link(int x,int y){
    split(x,y);//这里不是提取x-y的路径的意思,是makeroot+access+splay的偷懒写法
    si[f[x]=y]+=s[x];
    pushup(y);
}
//不保证
inline bool link(int x,int y){
    makeroot(x);
    if(findroot(y)==x)return 0;//access+splay已完成
    si[f[x]=y]+=s[x];
    pushup(y);
    return 1;
}

    cut(x,y):并无影响(其实理论上有影响,少了个儿子,然而这一操作也不会直接用来获取信息,下一次获取时信息会更新,不会影响正确性)

分析到此完毕。其实好像也就只改了一点点地方。。。。。。不过思路是很巧妙的,值得用心体会。
例题一

洛谷P4219 [BJOI2014]大融合(点击进入题目)
当学会了LCT维护子树信息和以后,这题就变得有些裸了。。。。。。
蒟蒻题解在这里
例题二

洛谷U19464 山村游历(Wander)(点击进入题目)
注:此题为WC模拟赛试题,版权归出题人Philipsweng所有。小蒟蒻觉得这题出得很不错,于是上传至洛谷个人题库,作为例题与大家分享。数据自测,如有问题欢迎反馈。
题目简述:
没有。。。。。。这是一个阅读题,简述的话就等于告诉你怎么做了
仔细读一下题目吧,这道题水平挺高的。
思路分析:
也有点复杂
然而分析完了以后,又变成裸的了。。。。。。
算了,还是单独建一篇随笔吧。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值