浅谈Link Cut Tree

借鉴的神犇看啊

动态树

其实很多人都把LCT和动态树搞混了,所以我们先阐明一下定义。

动态树问题

——就是会动的树上的问题(⊙v⊙)
会动——就是树的形态或权值是会变化的
常见问题形势——

维护两点的连通性
维护两点路径权值的和,最大值,最小值,自定义运算值……
维护LCA,直径,重心,自定义点与点的关系……
烦人的动态操作——

连接两个点(先前是不连通的)
把某个点变成某个点的父亲(先前是不连通的)
断开两个点的连接(先前是联通的)
修改点权,边权等等……
嗯,这种题一看就是裸数据结构对吧
所以就有了Tarjan发明的link-cut tree(OrzTarjen爷)

这个东西早在杨哲的集训队作业中提出了:QTREE解法的一些研究
上面的文段有很多正确且严谨的证明,下面不会再给出(OrzOrz)
如果通过上面的文段你已经理解了的话,就可以直接开始做题了!(Orz)

如果我没看懂呢?

没关系,你只需要两个知识:树链剖分、splay
首先我们需要了解什么是树链剖分
不会的话先看看吧,有益身心健康:大佬
(ps:这个博主很爷但是很奇怪,大家要小心)

splay的文章我就不引用了,大家随便百度看书都有

说白了,LCT就是用splay实现动态树的链剖分

我们会发现,树链剖分是用的线段树(至少上面那个是)
可是动态树就是因为会动而讨厌
嗯,如果我们把用线段树维护的东西改成用伸展树,以深度为顺序构建splay
听起来可行的样子
多么和谐!

可是具体怎么实现呢?

膜拜一下树链剖分,我们学着用splay维护每次访问的点到它所在树根的链
我们定义u的儿子中访问时间最后的点为w,若u的中以儿子v为根的子树包含了w,则称v为u的preferred child(就好像重儿子一样),自然,我们可以脑补出以下概念:
preferred road(u与v的连边)。
preferred path(连续的preferred road所表示的一条路径)
剩下的非preferred child的儿子吗,我们就像轻儿子一样,直接记个path father就好
这里注意一下,path father只能表示两颗splay之间的关系,不能表示两点的关系(通过下面的图你可以明白)
嗯,想像一下,一棵树原本被分成多个线段树,这些线段树又像树一样相连。

唰!线段树变成了splay!

听起来挺不错的样子是吧( ⊙ o ⊙ )

接下来怎么办?
别急,我们简单梳理一下
没错,我们把一颗树用splay剖分了
每颗splay对应着一条路径(preferred path)
(ps:我记得杨哲的paper里说的好像是个a什么什么 tree来着,我这里就直接用preferred path代替了)
splay之间构成了一片森林
这样的话原本要维护的森林就可以直接还原

问题来了
怎么取出某个点到所在树根的路径呢(⊙o⊙)?
我们要把x到根的路径变成一条preferred path!
这就用到了access操作

其实实现挺直接的,就是利用了splay的伸展性,无脑向上,splay不行就走path father,在路程中顺便更新下preferred path
实现就这几行
这里注意一下:
cutright操作只是在preferred path中断开,并不是在原树中断开(也就是说断开后,你要把右儿子的path father置为x)

这里写图片描述

有了access,我们可以做如下操作
1.断开x与其父亲的连边。
代码如下

void cut(int x)
{
    access(x);
    splay(x);//取出x到根的路径并将x变成路径根
    int l=splay[x].l;
    splay[x].l=0;//删除x的左孩子
    if (l)//如果左孩子存在,即x不为树根
        {
        splay[l].f=0;//断开左孩子和x的连边
        splay[l].pf=0;//包括path father也要清零(虽说其本来就是0)
        update(x);
        splay(l);//由于l变成了一棵树的根,所以splay它
        }
    return ;
}

相信通过代码和解释应该都能搞清楚怎么弄的了(^o^)/~
我们的splay是以深度为关键字的
如果是x和y的连边呢?
你可以稍微维护一下每个节点的父亲
通过access(x)和access(y)来区分我也不拦着你

2.判断x与y是否在同一棵树中
我们只需判断x和y所在的根就可以了
access(x)之后直接找最左(深度最小)的点就是啦O(∩_∩)O~~!
代码如下

int root(int x)
{
    access(x);
    splay(x);//取出x到根的路径并将x变成路径根
    while (splay[x].l) x=splay[x].l;//无脑找最左
    splay(x);//因为找出来的是树根,所以splay一发
    return x;
}

3.将x变成所在树的树根
代码很简单

void beroot(int x)
{
    access(x);
    splay(x);//说多了不想说了,以后的都直接省略
    splay[x].rev^=1;//给x所在路径打上翻转标记
    return ;
}

诶诶诶(O_O)?
这个翻转标记匪夷所思啊!

仔细一想,我们不过是将整条路径翻转了一下
路径是从x到根
翻转一下不就变成根到x了?
多么机智是不是^_^?

4.在x与y中连接一条边(这里是把y变成x的子树)
嗯,首先root(x)不等于root(y)
接着我们将y变成所在子树的根
把y的path father置为x
代码:

void join(int x,int y)
{
    int rootx=root(x),rooty=root(y);
    if (rootx==rooty)
        {
        //printf("加了就不是树了!!!");
        return ;
        }
    access(x);
    beroot(y);//变成根
    splay[y].pf=x;//接上x和y
    return ;
}

5.取出某条路径x到y的所查询权值
首先这条路径要有(root(x)==root(y))!(ps:在程序中我不会给出)
按照常理,我们先求LCA
先access(x)
再access(y)
将x所在preferred path的path father找出来,就是LCA
如果没有path father,则为x与y之间的一个
如果LCA就是根,那么只需access(x)、access(y)即可
否则我们先access(x),再access(LCA的父亲)
那么LCA所在preferred path就是x到LCA的路径
y类似
计算的时候别忘了对LCA的重复判断
代码

int LCA(int x,int y)
{
    access(x);
    access(y);
    splay(x);
    splay(y);//注意一下splay是放在两个access后
    if (!splay[x].pf)
        return deep[x]<deep[y]?x:y;//深度小的是LCA
    else
        return splay[x].pf;
}

int find(int x,int LCA)
{
    access(x);
    access(father(LCA));
    splay(LCA);//把LCA变成所在路径的根
    return splay[LCA].所需权值;
}

等等!
思考如下方法:
我们先将x变成整棵树的树根
再access(y)
如果是有根树我们在做完操作之后再把根变回去
这样,LCA就被我们完美避开了
代码:

int find(int x,int y)
{
    beroot(x);
    access(y);
    splayup(y);
    return splay[y].所需权值;
}

突然一下简洁了许多!(^o^)/~
程序不就是追求这个么?

好了,到这里基本的LCT你其实已经会了Y(^o^)Y
在练手之前,我提醒一下几点:
1.注意这颗splay是带区间翻转操作的
2.如果维护的值较多,请仔细理清权值之间的关系并且固定好顺序

嗯,练手题:
spoj 375 QTREE
这题是出了名的
维护一棵树支持两个操作
1.改变第i条边的权值
2.询问a到b的路径上的边权最大值
(ps:spoj上的QTREE系列题目都可以做一做)

山东省选2008 洞穴探测cave
维护一棵树支持三个操作
1.在x和y之间连一条边
2.将x与y的连边断开
3.询问x与y是否联通
输入数据保证操作合法

hdu4010
维护一棵树支持四个操作
1.在x与y之间连一条边
2.将x作为根,然后断开y与父亲的连边
3.将x到y的路径上所有点点权+k
4.询问x到y的路径上的点权最大值
每次操作若不合法请输出−1
如果你能一遍A这三题,说明你的LCT已经上路了(๑´ㅂ`๑)

一点使用技巧

一、边权的维护技巧

之前的题目都没有涉及到边权啊,失误失误(⊙o⊙)!
关于边权的维护,很多人会这么想:
反正一颗节点为w的树最多就w−1条边
嗯,除了根之外,其他的点全部捆绑上自己与父亲的连边进行维护
多么简单?

一点也不好么╮(╯_╰)╭
如果你去写写你就知道了
access会出大问题
1.path parent的修改会涉及到这条边的边权
2.当一条preferred path的根并不是这条链的顶端的时候,你会发现这个根绑定的权值和它的path parent的权值都需要维护(稍微不小心就会写错)
3.如果你不相信,你可以自己去实现实现,如果有简单的实现代码,请务必拿来让我膜拜一下/Orz

如果access出了问题,那么整个LCT就变得不好了╮(╯﹏╰)╭
嗯,其实我一开始也不知道怎么改进,后来我膜拜了其他Orz的代码

“把一条边拆成一个点,然后按照点维护不就好了么(ーー゛)?”大神这么说
怎么就这么机智呢?还是要Orz一下

来道题练练手吧:

NOI2014 魔法森林forest

在一个n个点的无向图上,每条边i有两个边权ai,bi
请你找出一条从1到n路径,使得两个权值的最大值之和最小

啥?这真的是动态树(O_O)?博主你在逗我吧?
嗯(⊙v⊙),这真的是动态树(虽然大牛们说自己都是写的spfa(ˇˍˇ))

思考如下做法:
1.我们按照某一个边权a从小到大排序,将其依次加入图中
2.如果两点不连通,则直接加入
3.如果两点连通,则将两点原本的路径上的边权b最大的边找出来,然后与新加的边比较,如果新边较大,则不加,否则就把找出来的边删除,然后加入新边
4.如果成功加入某条边之后1与n联通,则统计答案

算法的思想大致如下:
总之就是按照a的边权排个序,轮流加入,维护图的边权b的最小生成树
很贪心对不对?
为什么能贪心呢?
你的问题实际上等价于:为什么kruskal算法是对的
这个证明满大街都是,如果你会拟阵则更方便
(UOJ上你是可以看到AC的代码的,你可以膜拜,如果发现问题你也可以hack,嗯,总之UOJ大法好)

二、动态无向图的连通性维护

先来看道题:

上海省选2008 堵塞的交通traffic

题意我就不解释了,因为我并不会讲(这题正解是线段树)

考虑变式题,如果是一个无向图,这该怎么实现连通性的维护呢?
其实很简单
我们离线思考这个问题就会发现:
如果某个时刻两个点之间有多条路径,我们只需维护被破坏时间较后的那条路就可以了
也就是边权被破坏时间的最大生成树
嗯~(≧▽≦)/~,就是这样
如何维护最大生成树可以参考魔法森林的维护方法

妈妈如果我想维护最短路怎么办(=@__@=)?
这个……动态树真的可以做么(其实我也不知道)?
说不定要用到更高端的数据结构,说不定根本就不行
谁知道呢?
(翱犇告诉我:洗洗睡吧,好像没有有竞赛价值的做法)

一点总结

解决动态树的东西肯定是不止LCT这一个东西的,还有ETT(Eular-Tour Tree)
那么为什么非要讲这个呢?
当然是因为LCT是最容易实现的,也足够应付大部分动态树的题目了

额(⊙o⊙)…
如果你还不满足自己在动态树上的造诣的话
你可以去看看翱犇2014年的集训队论文《浅谈动态树的相关问题及简单拓展》(网上有2014集训队论文捆绑版)
如果你知道什么是仙人掌,你也可以去膜拜膜拜VFleaKing大神,并思考思考动态仙人掌(这个在UOJ和BZOJ上都有)

留下一个思考题吧,如果我们在对路径的权值修改操作的同时,加上对子树的权值修改操作,怎么办呢?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值