LCT(link cut tree) 详细图解与应用
樱雪喵用时 3days 做了 ybtoj 的 3 道例题,真是太有效率了!!1
为了避免自己没学明白就瞎写东西误人子弟,这篇 Blog 拖到了现在。
图片基本沿用 OIwiki,原文跳步骤(主要是 access 部分)的就自己补画了一些。
不过反正也没啥人看?
前置知识
Splay
欢迎阅读 Splay 详细图解 & 轻量级代码实现。
其实 LCT 对 Splay 的要求主要体现在维护序列的能力上,如果平时更习惯用其它平衡树,单纯为了学 LCT 去学 Splay 的话,“应用”那一段都不用看,会 rotate & splay & reverse 以及维护一些简单的序列操作(区间加区间乘?)就足够了。
据说无旋 Treap 也能用来维护 LCT,但复杂度多一个
log
?
\log
log,不建议大家这么写。不过如果真的痛恨 Splay 也可以去学学:Link
下文 Splay(大写)表示树,splay(小写)表示操作。
树链剖分
没写过博客,但应该没人学 LCT 不会这个吧。
动态树
动态树,顾名思义就是维护一个森林,支持连边和删边操作,要求维护树上的一些特定信息。
LCT(link cut tree),是一种解决动态树问题的数据结构。LCT 不叫动态树。
实链剖分
基于上文对动态树问题的概述,我们以 lg P3690 【模板】动态树(LCT) 为例看看 lct 具体用来干什么。
假设有这样的问题。
给一棵树,要求支持如下操作:
- 修改一个点的点权
- 查询路径 x x x 到 y y y 的点权异或和
不难想到,这个问题容易使用重链剖分在 O ( n log ? n ) O(n\log n) O(nlogn) 的时间复杂度内解决。
但如果这个森林是动态的,即改成支持如下操作:
- 修改一个点的点权
- 断开 x x x 和 y y y 之间的连边
- 在 x x x 和 y y y 之间连一条边,保证连边后还是一个森林
- 查询路径 x x x 到 y y y 的点权异或和
朴素的静态树链剖分难以维护这个问题,因为我们在改变树的结构时,树链剖分的形态也应随之改变。我们需要换一种方法来解决问题。
考虑重链剖分维护链上操作的本质,是给同一条链上的点赋上相邻的
d
f
n
dfn
dfn 编号,并保证从叶子到根经过的链的条数为
log
?
\log
log 级别,从而方便使用其他数据结构维护。
而在动态树中,问题的瓶颈变成了怎么让树链的划分状况随树的形态快速修改。考虑沿用重链剖分的思路,发现
s
i
z
e
size
size 是难以维护的;那我们显然更希望每个点的轻重儿子是我们自己指定的,而不是遵守特定的规则,这样在改变树形态的时候可以更自由地维护树链的变化。
我们把这种“自己指定轻重儿子”的剖分方式成为实链剖分,同一条链里的点之间连的边叫做实边,不同链之间的边叫虚边。其中每个点用实边相连的儿子叫实儿子,其余的叫虚儿子。
虽然这样划分是很自由的,但要注意它和树剖的区别:
- 每个非叶子节点至多有一个实儿子,但也可以一个都没有;
- 由上条可知,一条实链不一定要一直延伸到叶子节点。
现在,这棵树被我们划分成了若干实链。考虑使用 Splay 来分别维护这些链。对于一个链上的整体操作,我们就通过对这棵 Splay 维护区间操作来实现。
辅助树
在学习 LCT 的操作之前,要先了解辅助树的定义及其结构。
辅助树,就是维护每个树链的 Splay 之间通过某种方式相连接形成的树形结构。可以认为,Splay 维护的是一条实链,辅助树维护的是一棵树;一些辅助树放在一起就形成了 LCT,LCT 维护的是整个森林。
它满足如下性质:
- 辅助树由多棵 Splay 构成,其中每棵 Splay 维护的是原树的一条实链,且这棵 Splay 的中序遍历一一对应实链从上到下的点;
- 原树的每个点和辅助树上的点一一对应;
- 辅助树上的 Splay 之间通过某种方式连接成树形结构。具体地,我们原来学习的 Splay 根节点为空,但辅助树上的 Splay 不同,维护每条实链的 Splay,它根节点的父亲指向 这条实链顶端的点的父亲。注意不是指向 Splay 根节点在原树上的父亲。同时,对于根节点的父亲,它的两个儿子均不指向该点,这表示这条边是虚边。也就是说,所有实边是双向连接的;而虚边认父不认子,只能从儿子找父亲,父亲找不到儿子。我们可以根据这点来判断哪些点是 Splay 的根节点。
- 根据以上性质,我们在原树上所要进行的操作都可以在辅助树上进行,故下文的操作若无特别说明,均基于辅助树。
举个例子,一棵长成这样的树(实线为实边,虚线为虚边):
它的一种辅助树就可以长成这样(不认为这张图很难理解,所以直接贺过来了):
学习 Splay 时,我们记得同一个序列能构成的 Splay 有很多种不同形态,LCT 是同理的,对于相同的树形态和虚实边划分,由于 Splay 形态的不同,可以有很多种形态不同的辅助树。
那么我们得到了辅助树和原树之间的关系:
- 原树的实链都在辅助树的同一个 Splay 中;实边在 Splay 里中序遍历相邻,但不一定直接有边相连。
- 原树的虚边,由儿子所在 Splay 的根节点指向该点,但该点不指向虚儿子。
- Splay 上虽然至多只有两个实儿子,但剩下多出来的都是虚边,虚边可以有很多条。
- 原树的根不等于辅助树的根,辅助树在不破坏 Splay 性质的情况下可以随意换根。
- 原树的 father 指向和辅助树不同,注意区分。
- 在辅助树上,更易于进行虚实链之间的变换,这一点会在后文进行讲解。
常用函数的实现 - Splay 原有函数
知道了实链剖分和辅助树的概念,就可以开始实现 LCT 的一些常用函数了 qwq。
LCT 里使用的 Splay,由于根节点父亲不为空的定义与普通 Splay 不同,在写法上也需要做出一些改动。
我们先从简单的开始:
get
判断 x x x 是它父亲的哪个儿子。在 Splay 里面学习过了,不用改动。
il bool get(int x) {return rs(fa(x))==x;}
isroot
LCT 独有的函数,判断
x
x
x 是否为这棵 Splay 的根。
根据虚边认父不认子的性质,如果
x
x
x 父亲的两个儿子都找不到它,这条连边就是虚边,即
x
x
x 是 Splay 的根。
il bool isroot(int x) {return ls(fa(x))!=x&&rs(fa(x))!=x;}
pushup
在儿子节点改变时上传信息。以上文所述的题意为例,我们需要在 pushup 中更新这棵 Splay 的异或和。
il void pushup(int x)
{
// pushup other things here
tr[x].sum=tr[x].key^tr[ls(x)].sum^tr[rs(x)].sum;
}
pushdown
下传标记。
LCT 绝大部分时候都要在 Splay 上维护区间翻转标记,原因后文会讲。同时,根据题里的要求打的其他标记(这道题没有)也要一起下传。
这里和文艺平衡树一样,翻转标记有两种不同含义的写法:
- 一种是打标记的时候,这个点左右儿子已经换过了,标记表示它的两个儿子要换左右儿子;
- 另外一种是打标记表示这个点要换左右儿子,但现在还没换。
两种写法没有什么本质区别,但在后面一些地方 pushdown 的顺序写起来不一样。后文遇到有区别的地方会提到。
另外有些题,点
x
x
x 维护的信息与左右儿子的顺序有关。这个时候第二种就是错的,只能用第一种写法。 后文有相关例题。
这里为了代码简单,写的是第二种。
il void pushdown(int x)
{
// pushdown other tags here
if(!tr[x].lz) return;
swap(ls(x),rs(x)),tr[ls(x)].lz^=1,tr[rs(x)].lz^=1;
tr[x].lz=0;
}
update
update(x)
表示从
x
x
x 所在 Splay 的根开始,沿从根到
x
x
x 的链一路下放标记。
用于在 Splay(x)
前,保证
x
x
x 旋转时一路要经过的点的左右儿子都是正确的。
递归下放即可。
(upd: 文艺平衡树不需要这种东西是因为用来找对应排名节点的 find \text{find} find 操作把标记都下放了,但是 lct 不关心排名没有这个函数。)
il void update(int x)
{
if(!isroot(x)) update(fa(x));
pushdown(x);
}
rotate
与原义相同,把
x
x
x 向上旋转一层。
写法上和在 Splay 中学习的略有区别,因为我们要判断
y
y
y 是否是根,不能再简单地根据
z
z
z 的值是否为
0
0
0 来判断。
而把
y
y
y 和父亲的连边断开再判断会造成 isroot(y)
失效,所以我们把这句话提到前面:
il void rotate(int x)
{
int y=fa(x),z=fa(y),c=get(x);
if(!isroot(y)) tr[z].s[get(y)]=x; // here
fa(tr[x].s[c^1])=y,tr[y].s[c]=tr[x].s[c^1],tr[x].s[c^1]=y;
fa(y)=x,fa(x)=z; pushup(y),pushup(x);
}
Splay
把判断是否为根的语句换成 isroot 即可。注意 Splay 之前要先 update 这条路径,防止出现左右儿子顺序不对/未下传的标记跑到别的节点上的情况。
LCT 不需要形如把
x
x
x 转到某个点儿子的 Splay 函数,因为区间操作都是对整棵树操作。
il void splay(int x)
{
update(x);
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x))
if(!isroot(f)) rotate(get(f)==get(x)?f:x);
}
常用函数的实现 - LCT 的新函数
Splay 中出现过的基本函数到这里就讲完了,已经承包了 LCT 的绝大部分码量!
下文将对 LCT 特有的函数逐一讲解,或许不再像上文那么容易理解,但代码都非常简短(确信。
access
access(x)
的作用是把
x
x
x 到原树的根这条路径上的边都变成实链,同时其他原来与这条路径相连的实边都变成虚边。
换句话讲,把
x
x
x 到根的路径放到同一棵 Splay 里,而且这棵 Splay 里没有这条路径以外的点。
这是 lct 最基本的函数,写任何 lct 都必须要实现 access 操作。
注意要时刻分清楚原树的根和辅助树的根。
后面读到哪里发现读不懂就往上翻翻加粗的字,看看是不是什么地方混淆了概念 >_<(樱雪喵学的时候一直反反复复把概念搞混,所以看了一天才看懂(哭)
下面图比较多,为了避免来回翻页的痛苦把图缩小了(但貌似还是要来回翻页)。看不清可以单击放大,不过应该不至于(
还是以一开始举例子那张图为例,原树长这样:
辅助树长这样:
现在执行 access(N)
,我们希望它的原树变成这样:
当然辅助树会变成什么样我们还想象不出来,不过不急。
一步一步来。
我们要 access(N)
,那先把辅助树上的
N
N
N Splay 到根吧:
然后发现,要的是从
N
N
N 到原树的根的这条链,但是我们不想要
N
N
N 在原树上的儿子。原来的虚儿子当然不用管;只要把原来
N
N
N 的实儿子变成虚儿子就好了。
考虑原来
N
N
N 的实儿子在什么地方。根据 Splay 的性质,它的中序遍历是这个实链从上到下的点的顺序。那
N
N
N 在原树上的实儿子就是在现在的 Splay 上根节点的后继。
当然不用麻烦地去找后继在哪里,因为凡是
N
N
N 下面的链上的我们都不要。那直接把
N
N
N 的右儿子和
N
N
N 断开,改成虚边就可以了。
根据虚边认父不认子的性质,我们单方面地把 rs(N)
置为
0
0
0。
现在这条边变成了虚边:
接下来,我们已经根据
N
N
N 在辅助树上的的父亲,知道了链头在原树上的父亲是
I
I
I。也就是说我们想把
L
→
N
L\to N
L→N 这条链 接在
I
I
I 下面。
同样把
I
I
I splay 到根。这里 splay 的时候不会改变虚儿子指向
I
I
I 的虚边,一定要理解哪些边会跟着点的旋转跑,哪些不会(比如根的父亲这条边)。
现在我们希望
N
N
N 这棵树接在
I
I
I 下面,即
I
I
I 在 Splay 上的右儿子;但是
I
I
I 在 Splay 上已经有右儿子了,这意味着现在的
I
I
I 有实儿子。那么我们要把这个点
K
K
K 变成虚儿子,然后把
N
N
N 所在的这条链变成实儿子。
同样地,单方面令 rs(I)=N
即可。
现在
I
→
L
→
N
I\to L \to N
I→L→N 在一个实链里了。同理,我们按照上面的步骤继续。
因为
I
I
I 的父亲是
H
H
H,代表这条实链顶端的父亲是
H
H
H。所以我们把
H
H
H Splay 到根:
然后把
H
H
H 的右儿子换成
I
I
I:
继续 Splay(A)
并令 rs(A)=H
,相信大家能画明白怎么转,这里不分步了。
发现现在原树上
A
→
C
→
G
→
H
→
I
→
L
→
N
A\to C\to G\to H\to I\to L\to N
A→C→G→H→I→L→N 这条链正好在一棵 Splay 里了。
我们再回顾一下刚刚是怎么转的:
- 把当前节点 splay 到根;
- 令它的右儿子等于上次旋转的节点,并 pushup;
- 跳到当前点的父亲,重复以上步骤。
说了这么多,代码就一行, p p p 表示上次转的点。
il void access(int x) {for(int p=0;x;p=x,x=fa(x)) splay(x),rs(x)=p,pushup(x);}
makeroot
另一个重要的 lct 函数,写绝大部分题时都需要实现。不过确实有例外,比如 lg P4332 [SHOI2014] 三叉神经树 这种保证深度递增的题就不用写。
在维护一个路径信息时,往往不能保证路径深度递增,很多时候路径是先向上再向下的。根据辅助树的性质,这样的路径无法出现在同一棵 Splay 里。
但是我们还想维护它的信息,怎么办呢。
可以把原树的根换掉啊。makeroot(x)
的作用就是把
x
x
x 换成原树的根。
但是 Splay 里的点是按中序遍历从上到下存的,设原来的原树根是
r
t
rt
rt,考虑换成
x
x
x 会对哪些点的上下顺序产生影响:
只有
x
x
x 到
r
t
rt
rt 这条路径上的点上下顺序反过来了,剩下的都没变。上下顺序反过来,貌似就是把这一段的 Splay 区间翻转?
那我们先 access(x)
,把
x
x
x 到
r
t
rt
rt 弄到一棵 Splay 上。但你发现你不知道 access
完 Splay 的根是啥。所以 splay(x)
让
x
x
x 变成根你就知道了。最后在这个点上打 lazytag。
这就是为什么在 pushdown 里说 lct 的 splay 一般都要维护区间翻转标记。
il void makeroot(int x) {access(x),splay(x),tr[x].lz^=1;}
find
find(x)
的作用是查找
x
x
x 所在原树的根。
我们知道,access(x)
也是把
x
x
x 到原树的根这一段变成实链。那么先 access(x)
,再 splay(x)
,以
x
x
x 为根的这个 Splay 就表示从原树的根到
x
x
x 的实链。
根据 Splay 的性质,原树的根是这条链从上到下第一个点,也就是这棵 Splay 中序遍历里的第一个点。
Splay 中序遍历里的第一个点,不难发现就是
x
x
x 一直往左儿子走,直到没有左儿子,这个点就是原树的根。
注意我们打的 lazytag 表示还没有交换这个点的两个儿子,所以对每个点要先 pushdown 再判断有没有左儿子。
同理,如果你采用了 lazytag 的另一种定义,就把这个 pushdown 放到后面,因为这个点本身已经换过了。
找到根后要 splay(x)
来保证复杂度。(hack 形如 Splay 那篇文章的 rank 部分,一直查同一个很深的点)
il int find(int x)
{
access(x),splay(x);
while(pushdown(x),ls(x)) x=ls(x);
return splay(x),x;
}
split
split(x,y)
的作用是把
x
x
x 到
y
y
y 的路径拿出来变成一棵 Splay。
我们需要保证这条路径深度递增,所以先令
x
x
x 成为原树的根:makeroot(x);
接下来执行 access(y)
,就找到了这条路径。还有个问题是这样不知道 Splay 的根,所以后面一般会再做一步 splay(y)
。
il void split(int x,int y) {makeroot(x),access(y),splay(y);}
link
link(x,y)
表示在森林里给
x
x
x 和
y
y
y 之间连一条边。
那么我们先 makeroot(x)
,让
x
x
x 成为自己这棵树的根(原树、辅助树都是),然后判断它们两个是否已经连通。
如果不连通,直接把点
x
x
x 作为虚儿子单向指向
y
y
y 即可。
il void link(int x,int y) {makeroot(x);if(find(y)!=x) fa(x)=y;}
cut
cut(x,y)
表示删除森林中
x
x
x 到
y
y
y 的边。
我们同样先 makeroot(x)
。
这里要判断操作是否合法,当然可以用 map
一类的东西记录,但也可以从 Splay 结构的角度考虑。
x
x
x 到
y
y
y 有边,当且仅当:
- x x x 和 y y y 连通;
- x x x 到 y y y 的路径之间没有其他点;
前者可以用 find(y)==x
判断。对于后者,
y
y
y 和
x
x
x 直接相连,当且仅当
y
y
y 是
x
x
x 的右儿子,并且
y
y
y 没有左儿子。
你可能会奇怪,既然反正也要判后面这一半,前面还判 find(y)==x
干什么?
这里的 find(y)
里面同时隐含了 access(y)
和 splay(x)
的操作,是为了保证这两个点之间是实边、
x
x
x 是 Splay 的根的。
当然你手动拆出来写这两句话也行!
确定了
x
x
x 和
y
y
y 确实有边之后,我们令 fa(y)=rs(x)=0
,将它们双向断开。
il void cut(int x,int y)
{
makeroot(x);
if(find(y)==x&&fa(y)==x&&!ls(y)) fa(y)=rs(x)=0;
}
注意到这里的 pushup(x)
可以不写。看起来似乎不太符合常理,但仔细思考一下发现是对的:
考虑不 pushup 会对什么地方造成影响。因为
x
x
x 一定是 Splay 的根节点,所以它不 pushup 只会造成它自己的值不对。我们考虑接下来的操作。
- 如果是查询(split),那会对根节点进行 access 操作,access 里面有 pushup;
- 如果是修改树的形态, x x x 被 rotate 下去的时候也会被 pushup。
那么我们成功证明了这么做的正确性。
时间复杂度证明
记住是 O ( n log ? n ) O(n\log n) O(nlogn) 就行,如果想研究怎么证明的话可以看 Link。
完整代码 & 一些建议
所有 LCT 的基本操作到这里就讲完了。这里给出一个 lg P3690 【模板】动态树(LCT) 的代码。
可以看出来比 Splay 板子短好多。
#include<bits/stdc++.h>
#define il inline
using namespace std;
il int read()
{
int xr=0,F=1; char cr;
while(cr=getchar(),cr<'0'||cr>'9') if(cr=='-') F=-1;
while(cr>='0'&&cr<='9')
xr=(xr<<3)+(xr<<1)+(cr^48),cr=getchar();
return xr*F;
}
const int N=3e5+5;
struct node
{
int key,sum,fa,lz;
int s[2];
node() {key=sum=fa=lz=s[0]=s[1]=0;}
}tr[N];
#define ls(x) tr[(x)].s[0]
#define rs(x) tr[(x)].s[1]
#define fa(x) tr[(x)].fa
il bool get(int x) {return rs(fa(x))==x;}
il bool isroot(int x) {return ls(fa(x))!=x&&rs(fa(x))!=x;}
il void pushup(int x) {tr[x].sum=tr[ls(x)].sum^tr[rs(x)].sum^tr[x].key;}
il void pushdown(int x)
{
if(!tr[x].lz) return;
swap(ls(x),rs(x)),tr[ls(x)].lz^=1,tr[rs(x)].lz^=1;
tr[x].lz=0;
}
il void update(int x)
{
if(!isroot(x)) update(fa(x));
pushdown(x);
}
il void rotate(int x)
{
int y=fa(x),z=fa(y),c=get(x);
if(!isroot(y)) tr[z].s[get(y)]=x;
fa(tr[x].s[c^1])=y,tr[y].s[c]=tr[x].s[c^1],tr[x].s[c^1]=y;
fa(y)=x,fa(x)=z; pushup(y),pushup(x);
}
il void splay(int x)
{
update(x);
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x))
if(!isroot(f)) rotate(get(f)==get(x)?f:x);
}
il void access(int x) {for(int p=0;x;p=x,x=fa(x)) splay(x),rs(x)=p,pushup(x);}
il void makeroot(int x) {access(x),splay(x),tr[x].lz^=1;}
il int find(int x)
{
access(x),splay(x);
while(pushdown(x),ls(x)) x=ls(x);
return splay(x),x;
}
il void link(int x,int y) {makeroot(x);if(find(y)!=x) fa(x)=y;}
il void split(int x,int y) {makeroot(x),access(y),splay(y);}
il void cut(int x,int y)
{
makeroot(x);
if(find(y)==x&&fa(y)==x&&!ls(y)) fa(y)=rs(x)=0;
}
int main()
{
int n=read(),m=read();
for(int i=1;i<=n;i++) tr[i].key=read();
while(m--)
{
int op=read(),x=read(),y=read();
if(op==0) split(x,y),printf("%d\n",tr[y].sum);
if(op==1) link(x,y);
if(op==2) cut(x,y);
if(op==3) makeroot(x),splay(x),tr[x].key=y;
}
return 0;
}
说一点本喵调了几天 LCT 获得的经验。
基本的板子一定要记熟。就算一开始记不住可以对着原来的看,但打个几遍总该差不多背下来。
理解原理然后不特地去背,自己就能写出来当然是很神仙的;但是写挂了调起来确实麻烦(某天手画了一整张纸的 splay 试图找哪里有锅,最后是 rotate 写错了)。
或许这个笨办法只适用于没有脑子的樱雪喵?
LCT 的各种应用
维护树链信息
比较板的应用。一般来说就是在动态树上维护某些树链之间的信息,比如区间和之类的东西。
LCT 的写法在下面三个例题里都是完全一致的,区别只有 pushup pushdown 以及 Splay 维护的东西不同。
P3690 【模板】动态树(LCT)
题意:动态树,支持加边删边,单点修改,查询路径上的点权异或和。
上文说过的板子题,代码见上文。
P1501 [国家集训队] Tree II
题意:支持加边删边,对一条路径区间加 / 区间乘,查询路径点权和。
对一条路径进行区间加 / 区间乘,可以仿照 【模板】线段树 2 的方法,给 Splay 的节点分别打加法和乘法标记。
但是 Splay 和线段树不一样的地方在于子树
s
i
z
e
size
size 不固定,所以也要单独维护;且 Splay 的区间是 左子树+自己+右子树,线段树少自己这一项,在 pushup 的时候要根据这点适当改动。
坑点是模数看起来很小,但平方会爆 int。
#include<bits/stdc++.h>
#define il inline
using namespace std;
il int read()
{
int xr=0,F=1; char cr;
while(cr=getchar(),cr<'0'||cr>'9') if(cr=='-') F=-1;
while(cr>='0'&&cr<='9')
xr=(xr<<3)+(xr<<1)+(cr^48),cr=getchar();
return xr*F;
}
#define int long long
const int N=1e5+5,mod=51061;
struct node
{
int siz,lz,sum,key,addtag,multag;
int s[2],fa;
node() {siz=lz=sum=key=addtag=s[0]=s[1]=fa=0;multag=1;}
}tr[N];
#define ls(x) tr[(x)].s[0]
#define rs(x) tr[(x)].s[1]
#define fa(x) tr[(x)].fa
il bool get(int x) {return x==rs(fa(x));}
il bool isroot(int x) {return ls(fa(x))!=x&&rs(fa(x))!=x;}
il void pushup(int x)
{
tr[x].siz=tr[ls(x)].siz+tr[rs(x)].siz+1;
tr[x].sum=(tr[ls(x)].sum+tr[rs(x)].sum+tr[x].key)%mod;
}
il void pushdown(int x)
{
if(tr[x].lz)
{
swap(ls(ls(x)),rs(ls(x))),swap(ls(rs(x)),rs(rs(x)));
tr[ls(x)].lz^=1,tr[rs(x)].lz^=1;
tr[x].lz=0;
}
int mult=tr[x].multag,addt=tr[x].addtag;
(tr[ls(x)].sum*=mult)%=mod,(tr[ls(x)].key*=mult)%=mod;
(tr[rs(x)].sum*=mult)%=mod,(tr[rs(x)].key*=mult)%=mod;
(tr[ls(x)].addtag*=mult)%=mod,(tr[ls(x)].multag*=mult)%=mod;
(tr[rs(x)].addtag*=mult)%=mod,(tr[rs(x)].multag*=mult)%=mod;
tr[x].multag=1;
(tr[ls(x)].sum+=addt*tr[ls(x)].siz)%=mod,(tr[ls(x)].key+=addt)%=mod;
(tr[rs(x)].sum+=addt*tr[rs(x)].siz)%=mod,(tr[rs(x)].key+=addt)%=mod;
(tr[ls(x)].addtag+=addt)%=mod,(tr[rs(x)].addtag+=addt)%=mod;
tr[x].addtag=0;
}
il void update(int x)
{
if(!isroot(x)) update(fa(x));
pushdown(x);
}
il void rotate(int x)
{
int y=fa(x),z=fa(y),c=get(x);
if(!isroot(y)) tr[z].s[get(y)]=x;
fa(tr[x].s[c^1])=y,tr[y].s[c]=tr[x].s[c^1],fa(y)=x,tr[x].s[c^1]=y;
fa(x)=z; pushup(y),pushup(x);
}
il void splay(int x)
{
update(x);
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x))
if(!isroot(f)) rotate(get(f)==get(x)?f:x);
}
il void access(int x) {for(int p=0;x;p=x,x=fa(x)) splay(x),rs(x)=p,pushup(x);}
il void makeroot(int x) {access(x),splay(x);swap(ls(x),rs(x)),tr[x].lz^=1;}
il void split(int x,int y) {makeroot(x),access(y),splay(y);}
il int find(int x)
{
access(x),splay(x);
while(ls(x)) x=ls(x),pushdown(x);
return splay(x),x;
}
il void link(int x,int y) {makeroot(x);if(find(y)!=x) fa(x)=y,pushup(y);}
il void cut(int x,int y)
{
makeroot(x);
if(find(y)==x&&fa(y)==x&&!ls(y)) rs(x)=fa(y)=0,pushup(x);
}
signed main()
{
int n=read(),q=read();
for(int i=1;i<=n;i++) tr[i].key=1;
for(int i=1;i<n;i++)
{
int u=read(),v=read();
link(u,v);
}
while(q--)
{
char c;cin>>c;
if(c=='+')
{
int x=read(),y=read(),c=read();
split(x,y),(tr[y].key+=c)%=mod,(tr[y].sum+=c*tr[y].siz)%=mod;
(tr[y].addtag+=c)%=mod;
}
if(c=='-')
{
int u=read(),v=read(),x=read(),y=read();
cut(u,v),link(x,y);
}
if(c=='*')
{
int x=read(),y=read(),c=read();
split(x,y);
(tr[y].key*=c)%=mod,(tr[y].sum*=c)%=mod;
(tr[y].multag*=c)%=mod,(tr[y].addtag*=c)%=mod;
}
if(c=='/')
{
int x=read(),y=read();
split(x,y);
printf("%lld\n",tr[y].sum);
}
}
return 0;
}
P4842 城市旅行
题意:动态树,支持对一条路径区间加,求在一条给定路径上随机选两个点,这两个点之间路径点权和的期望。
如果你觉得上一道题 Splay 打标记打得不够过瘾,可以写写这个题。
期望是个幌子,实际就是让你维护这条路径的每条子路径的点权和的和。考虑路径上第
x
x
x 个点的贡献,设路径长度为
n
n
n,那么它的贡献是
a
x
×
(
x
)
(
n
?
x
+
1
)
a_x\times(x)(n-x+1)
ax?×(x)(n?x+1)。
然后用 Splay 维护这个东西。具体柿子不推了,可以去看题解。
总之最后要维护
s
i
z
,
r
e
v
,
a
d
d
t
a
g
,
k
e
y
,
r
e
s
,
s
u
m
,
s
u
m
l
,
s
u
m
r
siz,rev,addtag,key,res,sum,suml,sumr
siz,rev,addtag,key,res,sum,suml,sumr。祝你能调出来(。
Warning. 这个题就属于之前说的“维护的信息与左右儿子的顺序有关”的一类,翻转标记只能用第一种写法。下放翻转标记的时候别忘了把 s u m l suml suml 和 s u m r sumr sumr 也一起颠倒过来。
点击查看代码 ``` 不知道触发了什么神秘特性,这段代码一粘进去博客就崩。 只能放提交记录了。 https://www.luogu.com.cn/record/124594260 ```P4332 [SHOI2014] 三叉神经树
题意:给一棵树,每个点有三个儿子。除叶子节点的黑白已给定外,每个点的颜色是儿子中较多的那一种颜色。修改叶子节点,问根节点颜色。
和上面三个题不太一样,这道题里的 Splay 维护的是最深的恰有 1/2 个儿子为黑色的节点编号。
是比较罕见的不用写 makeroot 和 reverse 的 lct。
#include<bits/stdc++.h>
#define il inline
using namespace std;
il int read()
{
int xr=0,F=1; char cr;
while(cr=getchar(),cr<'0'||cr>'9') if(cr=='-') F=-1;
while(cr>='0'&&cr<='9')
xr=(xr<<3)+(xr<<1)+(cr^48),cr=getchar();
return xr*F;
}
const int N=1.5e6+5;
struct node
{
int sum,lz,res1,res2;
int s[2],fa;
node() {sum=fa=lz=res1=res2=s[0]=s[1]=0;}
}tr[N];
#define ls(x) tr[(x)].s[0]
#define rs(x) tr[(x)].s[1]
#define fa(x) tr[(x)].fa
il bool get(int x) {return x==rs(fa(x));}
il bool isroot(int x) {return x!=ls(fa(x))&&x!=rs(fa(x));}
il void pushup(int x)
{
tr[x].res1=tr[rs(x)].res1,tr[x].res2=tr[rs(x)].res2;
if(!tr[x].res1)
{
if(tr[x].sum!=1) tr[x].res1=x;
else tr[x].res1=tr[ls(x)].res1;
}
if(!tr[x].res2)
{
if(tr[x].sum!=2) tr[x].res2=x;
else tr[x].res2=tr[ls(x)].res2;
}
}
il void pushdown(int x)
{
if(!tr[x].lz) return;
tr[ls(x)].sum+=tr[x].lz,tr[rs(x)].sum+=tr[x].lz;
swap(tr[ls(x)].res1,tr[ls(x)].res2);
swap(tr[rs(x)].res1,tr[rs(x)].res2);
tr[ls(x)].lz+=tr[x].lz,tr[rs(x)].lz+=tr[x].lz;
tr[x].lz=0;
}
il void update(int x)
{
if(!isroot(x)) update(fa(x));
pushdown(x);
}
il void rotate(int x)
{
int y=fa(x),z=fa(y),c=get(x);
if(!isroot(y)) tr[z].s[get(y)]=x;
fa(tr[x].s[c^1])=y,tr[y].s[c]=tr[x].s[c^1],fa(y)=x,tr[x].s[c^1]=y;
fa(x)=z; pushup(y),pushup(x);
}
il void splay(int x)
{
update(x);
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x))
if(!isroot(f)) rotate(get(f)==get(x)?f:x);
}
il void access(int x) {for(int p=0;x;p=x,x=fa(x)) splay(x),rs(x)=p,pushup(x);}
int n;
vector<int> E[N];
void dfs(int u)
{
for(auto v:E[u]) dfs(v),tr[u].sum+=(tr[v].sum>1);
}
int main()
{
n=read();
for(int i=1;i<=n;i++)
{
for(int j=1;j<=3;j++)
{
int x=read();
E[i].push_back(x),tr[x].fa=i;
}
}
for(int i=n+1;i<=3*n+1;i++) tr[i].sum=read()+1;
dfs(1);
// cout<<tr[1].sum<<endl;
int q=read();
while(q--)
{
int x=read(),f=fa(x);
// cout<<"x= "<<x<<" "<<f<<endl;
if(tr[x].sum==1) //0->1
{
access(f),splay(f);
if(tr[f].res1)
{
int now=tr[f].res1;
splay(now);
tr[now].sum++,tr[rs(now)].sum++,tr[rs(now)].lz++;
swap(tr[rs(now)].res1,tr[rs(now)].res2),pushup(now);
}
else tr[f].sum++,tr[f].lz++,swap(tr[f].res1,tr[f].res2);
}
else
{
access(f),splay(f);
if(tr[f].res2)
{
int now=tr[f].res2;
splay(now);
tr[now].sum--,tr[rs(now)].sum--,tr[rs(now)].lz--;
swap(tr[rs(now)].res1,tr[rs(now)].res2),pushup(now);
}
else tr[f].sum--,tr[f].lz--,swap(tr[f].res1,tr[f].res2);
}
tr[x].sum=(tr[x].sum==1)?2:1,splay(1);
printf("%d\n",tr[1].sum>1?1:0);
}
return 0;
}
维护连通性
lg P2147 [SDOI2008] 洞穴勘测
给一堆点,支持加边删边,保证始终是森林,询问两点是否连通。
之前写 link 的时候就用到过判断连通,先 makeroot 再 find 就可以了。
这里应当有一些别的题,但是先摆。
#include<bits/stdc++.h>
#define il inline
using namespace std;
il int read()
{
int xr=0,F=1; char cr=getchar();
while(cr<'0'||cr>'9') {if(cr=='-') F=-1;cr=getchar();}
while(cr>='0'&&cr<='9')
xr=(xr<<3)+(xr<<1)+(cr^48),cr=getchar();
return xr*F;
}
const int N=2e5+5;
#define ls(x) tr[(x)].s[0]
#define rs(x) tr[(x)].s[1]
struct tree{
int fa,lz,s[2];
}tr[N];
int n,m;
void push_down(int x)
{
if(!tr[x].lz) return;
swap(ls(ls(x)),rs(ls(x))),swap(ls(rs(x)),rs(rs(x)));
tr[ls(x)].lz^=1,tr[rs(x)].lz^=1;
tr[x].lz=0;
}
bool get(int x) {return x==tr[tr[x].fa].s[1];}
bool isroot(int x) {return ls(tr[x].fa)!=x&&rs(tr[x].fa)!=x;}
void rotate(int x)
{
int y=tr[x].fa,z=tr[y].fa,chk=get(x);
if(!isroot(y)) tr[z].s[get(y)]=x;
if(tr[x].s[chk^1]) tr[tr[x].s[chk^1]].fa=y;
tr[y].s[chk]=tr[x].s[chk^1];tr[x].s[chk^1]=y,tr[y].fa=x;
tr[x].fa=z;
}
void update(int x)
{
if(!isroot(x)) update(tr[x].fa);
push_down(x);
}
void splay(int x)
{
update(x);
for(int f=tr[x].fa;f=tr[x].fa,!isroot(x);rotate(x))
if(!isroot(f)) rotate(get(f)==get(x)?f:x);
}
void access(int x) {for(int p=0;x;p=x,x=tr[x].fa) splay(x),rs(x)=p;}
void make_root(int x) {access(x),splay(x);swap(ls(x),rs(x)),tr[x].lz^=1;}
int find(int x)
{
access(x),splay(x);
while(ls(x)) push_down(x),x=ls(x);
splay(x);return x;
}
void link(int x,int y) {make_root(x);if(find(y)!=x) tr[x].fa=y;}
void cut(int x,int y)
{
make_root(x);
if(find(y)==x&&tr[y].fa==x&&(!ls(y))) tr[y].fa=rs(x)=0;
}
int main()
{
n=read(),m=read();
while(m--)
{
string s;cin>>s;
int x=read(),y=read();
if(s=="Query")
{
if(find(x)==find(y)) printf("Yes\n");
else printf("No\n");
}
else if(s=="Connect") link(x,y);
else cut(x,y);
}
return 0;
}
维护生成树
对于维护边权相关的问题,我们发现由于 LCT 的灵活性很高,同时辅助树上的边也与原树的边不相同,因此无法简单地通过点权来定位一条边的边权。
解决方法是,我们新建一些点,令每个点代表一条边,边权记在对应点的点权上。那么每次连边删边时,我们分别 link 和 cut 两次,把边的两个端点和代表边权的点分别连接 / 断开。
P4234 最小差值生成树
题意:给 n n n 个点 m m m 条边的图,求最大最小边权之差最小的生成树。
考虑从小到大加入边,并维护当前情况下最小边权的最大值。
- 如果当前 u , v u,v u,v 已经连通,那么在 u , v u,v u,v 的路径上找到边权最小的边,并把它删掉,改为加入当前的新边;
- 如果当前 u , v u,v u,v 不连通,直接加入这条边。
对于一条路径上边权最小的边,转为点权后就是一棵 Splay 里点权最小的点。所以只需在 Splay 里维护点权最小的编号。
点击查看代码#include<bits/stdc++.h>
#define il inline
using namespace std;
il int read()
{
int xr=0,F=1; char cr;
while(cr=getchar(),cr<'0'||cr>'9') if(cr=='-') F=-1;
while(cr>='0'&&cr<='9')
xr=(xr<<3)+(xr<<1)+(cr^48),cr=getchar();
return xr*F;
}
const int N=3e5+5,inf=1e9;
struct node
{
int key,id,lz;
int s[2],fa;
}tr[N];
#define ls(x) tr[(x)].s[0]
#define rs(x) tr[(x)].s[1]
#define fa(x) tr[(x)].fa
il bool get(int x) {return x==rs(fa(x));}
il bool isroot(int x) {return x!=ls(fa(x))&&x!=rs(fa(x));}
il void pushup(int x)
{
tr[x].id=x,tr[0].id=0,tr[0].key=inf;
if(tr[tr[ls(x)].id].key<tr[tr[x].id].key) tr[x].id=tr[ls(x)].id;
if(tr[tr[rs(x)].id].key<tr[tr[x].id].key) tr[x].id=tr[rs(x)].id;
}
il void pushdown(int x)
{
if(!tr[x].lz) return;
swap(ls(x),rs(x)),tr[ls(x)].lz^=1,tr[rs(x)].lz^=1;
tr[x].lz=0;
}
il void update(int x)
{
if(!isroot(x)) update(fa(x));
pushdown(x);
}
il void rotate(int x)
{
int y=fa(x),z=fa(y),c=get(x);
if(!isroot(y)) tr[z].s[get(y)]=x;
fa(tr[x].s[c^1])=y,tr[y].s[c]=tr[x].s[c^1],tr[x].s[c^1]=y;
fa(y)=x,fa(x)=z; pushup(y),pushup(x);
}
il void splay(int x)
{
update(x);
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x))
if(!isroot(f)) rotate(get(f)==get(x)?f:x);
}
il void access(int x) {for(int p=0;x;p=x,x=fa(x)) splay(x),rs(x)=p,pushup(x);}
il void makeroot(int x) {access(x),splay(x),tr[x].lz^=1;}
il void split(int x,int y) {makeroot(x),access(y),splay(y);}
il int find(int x)
{
access(x),splay(x);
while(pushdown(x),ls(x)) x=ls(x);
return splay(x),x;
}
il void link(int x,int y) {makeroot(x);if(find(y)!=x) fa(x)=y;}
il void cut(int x,int y)
{
makeroot(x);
if(find(y)==x&&fa(y)==x&&!ls(y)) rs(x)=fa(y)=0,pushup(x);
}
int n,m;
struct edge {int u,v,w;}e[N];
bool cmp(edge x,edge y) {return x.w<y.w;}
bool flag[N];
int ans=1,mn=inf;
int main()
{
n=read(),m=read();
for(int i=1;i<=m;i++) e[i]={read(),read(),read()};
sort(e+1,e+m+1,cmp);
for(int i=1;i<=n;i++) tr[i].key=inf;
for(int i=1;i<=m;i++) tr[i+n].key=e[i].w;
int now=0;
for(int i=1;i<=m;i++)
{
// cerr<<"qwq "<<i<<endl;
int u=e[i].u,v=e[i].v;
if(u==v) continue;
if(find(u)!=find(v))
{
link(u,i+n),link(i+n,v),flag[i]=1,now++;
}
else
{
split(u,v);
int id=tr[v].id; flag[id-n]=0;
// cout<<id<<" "<<tr[id].key<<endl;
cut(id,e[id-n].u),cut(id,e[id-n].v);
link(i+n,u),link(i+n,v);
flag[i]=1;
}
while(!flag[ans]) ans++;
if(now==n-1) mn=min(mn,e[i].w-e[ans].w);
}
printf("%d\n",mn);
return 0;
}
P2387 [NOI2014] 魔法森林
和上一题本质相同,但是这里可能会出现路径上已有的最大边比当前边小的情况,注意判断。
很不理解为什么 ybtoj 要把这玩意放在板子后面的第一道例题,我第一次对着 ybtoj 学 lct 就是被这东西劝退的 >_<
#include<bits/stdc++.h>
#define il inline
using namespace std;
il int read()
{
int xr=0,F=1; char cr;
while(cr=getchar(),cr<'0'||cr>'9') if(cr=='-') F=-1;
while(cr>='0'&&cr<='9')
xr=(xr<<3)+(xr<<1)+(cr^48),cr=getchar();
return xr*F;
}
const int N=1.5e5+5,inf=1e9;
struct node
{
int id,key,lz;
int s[2],fa;
}tr[N];
#define ls(x) tr[(x)].s[0]
#define rs(x) tr[(x)].s[1]
#define fa(x) tr[(x)].fa
il bool get(int x) {return x==rs(fa(x));}
il bool isroot(int x) {return x!=ls(fa(x))&&x!=rs(fa(x));}
il void pushup(int x)
{
tr[x].id=x;
if(tr[tr[ls(x)].id].key>tr[tr[x].id].key) tr[x].id=tr[ls(x)].id;
if(tr[tr[rs(x)].id].key>tr[tr[x].id].key) tr[x].id=tr[rs(x)].id;
}
il void pushdown(int x)
{
if(!tr[x].lz) return;
swap(ls(x),rs(x)),tr[ls(x)].lz^=1,tr[rs(x)].lz^=1;
tr[x].lz=0;
}
il void update(int x)
{
if(!isroot(x)) update(fa(x));
pushdown(x);
}
il void rotate(int x)
{
int y=fa(x),z=fa(y),c=get(x);
if(!isroot(y)) tr[z].s[get(y)]=x;
fa(tr[x].s[c^1])=y,tr[y].s[c]=tr[x].s[c^1],tr[x].s[c^1]=y;
fa(y)=x,fa(x)=z; pushup(y),pushup(x);
}
il void splay(int x)
{
update(x);
for(int f=fa(x);f=fa(x),!isroot(x);rotate(x))
if(!isroot(f)) rotate(get(f)==get(x)?f:x);
}
il void access(int x) {for(int p=0;x;p=x,x=fa(x)) splay(x),rs(x)=p,pushup(x);}
il void makeroot(int x) {access(x),splay(x);tr[x].lz^=1;}
il void split(int x,int y) {makeroot(x),access(y),splay(y);}
il int find(int x)
{
access(x),splay(x);
while(pushdown(x),ls(x)) x=ls(x);
return splay(x),x;
}
il void link(int x,int y) {makeroot(x);if(find(y)!=x) fa(x)=y;}
il void cut(int x,int y)
{
makeroot(x);
if(find(y)==x&&fa(y)==x&&!ls(y)) rs(x)=fa(y)=0,pushup(x);
}
bool flag[N];
int n,m,ans=inf;
struct edge{int u,v,a,b;}e[N];
bool cmp(edge x,edge y)
{
if(x.a==y.a) return x.b<y.b;
else return x.a<y.a;
}
int main()
{
n=read(),m=read();
for(int i=1;i<=m;i++) e[i]={read(),read(),read(),read()};
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++) tr[i+n].key=e[i].b;
for(int i=1;i<=m;i++)
{
int u=e[i].u,v=e[i].v;
if(u==v) continue;
if(find(u)!=find(v))
{
link(u,i+n),link(v,i+n);
}
else
{
split(u,v);
int id=tr[v].id;
if(e[id-n].b>=e[i].b)
{
cut(e[id-n].u,id),cut(e[id-n].v,id);
link(e[i].u,i+n),link(e[i].v,i+n);
}
}
if(find(1)==find(n))
{
split(1,n);
int mx=e[tr[n].id-n].b;
ans=min(ans,e[i].a+mx);
}
}
if(ans==inf) printf("-1\n");
else printf("%d\n",ans);
return 0;
}
看起来耗时一天终于写完了,寒假只会贺题解的东西一年后才补,我好菜啊。
有问题就评论区指出吧(
完结撒花 >w<
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!