动态树LCT(Link-cut-tree)总结+模板题+各种题目

时间:2021-08-24 13:12:53

一、理解LCT的工作原理

先看一道例题:

让你维护一棵给定的树,需要支持下面两种操作:

Change x val:  令x点的点权变为val

Query x y:  计算x,y之间的唯一的最短路径的点权的xor和

这是一道树剖裸题。我们知道,当题目中出现了维护与树上最短路相关的区间操作时,基本可以确定要用树剖来做了。

再来看一下这道题的升级版:

让你维护一棵给定的树,需要支持下面四种操作:

Change x val:  令x点的点权变为val

Query x y:  计算x,y之间的唯一的最短路径的点权xor和

Cut x y:  如果x,y间有边相连,则删除它。

Link x y:  如果x,y不联通,则建立一条x,y之间的有向边。

在这道题里,我们增加了两个操作,Link和Cut。我们发现,这道题不可以用树剖来做了——显然,树剖无法处理修改树的形状的相关操作的。

现在我们就需要LCT了。

  LCT,全称Link Cut Tree,中文名“动态树”。顾名思义,这种数据结构就是支持连边和断边操作同时像树剖那样维护一些数据的树。由于需要支持连边断边,LCT就不能像树链剖分一样用线段树来维护了,而需要使用更加灵活的延展树(Splay)。

  因此,与树链剖分一样,LCT需要满足以下这些性质:

  1.每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增。

//只要把树剖和Splay搞懂了,这一点不难理解。

  2.每一个节点存在且只存在于一个Splay中

  3.边分为实边和虚边。实边包含在Splay树中中序遍历的相邻节点之间,而虚边必须由一棵Splay指向另一个节点,也就是中序遍历中最靠前的哪个节点的父亲。

//这里需要注意的是,一般LCT的虚边是用Splay的根节点的father指针来实现的。

  辅助理解

当一棵树的虚实边是这样分配的时候,它所对应的伸展树是这个样子:

动态树LCT(Link-cut-tree)总结+模板题+各种题目  动态树LCT(Link-cut-tree)总结+模板题+各种题目

其中,每一个虚线框起来的区域都是一棵伸展树;双向箭头指父节点存son,子节点存father的边;单项箭头指子节点存father,而父节点不存son的边(虚边)。


二、具体操作函数

1.access(int u) 具体功能:把节点u到根节点的路径压入一个Splay中(即标记为重边)

这个没图的话实在难理解。仍然举一个例子:

如果我们在上图的情况下对节点M执行这一操作,那么这棵树就会有下图的变化:

动态树LCT(Link-cut-tree)总结+模板题+各种题目 动态树LCT(Link-cut-tree)总结+模板题+各种题目

根据样例我们可以看出,access的操作大概可以分两步:

首先,Splay(M),将M旋转到Splay的根节点;

然后,使节点M替代它的父亲G的右子的位置,即G->rs=M。

这时,原本G的右子脱离了G所在的伸展树,与G连接的边变为了虚边;而节点M对应的伸展树则与G所在的伸展树合并,M和G之间的虚边变成了实边。

循环执行这两个操作,直到到达根节点,access操作就完成了。

这里需要注意的是,当完成一次连实边操作之后,需要将它的父亲pushup,以保证其维护数据的正确性。

这样,我们就得到了access的代码:

inline void access(int u) {
for(int v=;u;v=u,u=father[u])
Splay(u), rs[u]=v, pushup(u);
}

access

2.makeroot(int u) 具体功能:令节点u成为其所在树的根节点

当我们需要两个点之间的路径信息的时候,应该怎么做?

如果其中一个点不是另一个点的祖先的话,它们是不可能在同一个Splay里面的。

这时,我们就需要makeroot函数把其中一个点搞成根节点了。执行完access(u)操作之后,u会成为所在Splay深度最大的点;然后只需要把整棵树翻转,u就是整棵树的根了。

代码如下:

inline void makeroot(int u) {
access(u); Splay(u); reverse(u);
}

makeroot

3.split(int u,int v) 具体功能:把节点u到节点v间的路径压入到一个Splay中

有了makeroot之后,split变得超级简单:

inline void split(int u,int v) {
makeroot(u); access(v); Splay(v);
}

split

4.findroot(int u) 具体功能:查询该节点所在树的根节点编号

主要应用于判断连通性。

inline int findroot(int u) {
access(u); Splay(u); pushdown(u);
while(ls[u]) pushdown(u),u=ls[u];
return u;
}

findroot

5.link(int u,int v)

当u-v不连通时,连一条u到v的轻边。

inline void link(int u,int v) {
makeroot(u);
if(findroot(v)!=u)
father[u]=v;
}

link

6.cut(int u,int v)

根据LCT的性质,我们可以发现,当makeroot(u),access(v)之后,如果存在边u-v,必须满足以下条件:

(1)u-v联通

(2)u-v在同一条Splay里有父子关系

(3)u-v在Splay的中序遍历中相邻

这样判断是否有u-v的逻辑语句就完成了,直接删除即可。

代码如下:

inline void cut(int u,int v) {
makeroot(u);
if(findroot(v)==u && father[u]==v && rs[u]==)
father[u]=ls[v]=,pushup(v);
}

cut

7. 伸展树

这个伸展树比一般Splay特殊一些。它的判根、rotate操作和Splay操作之前的pushall函数需要注意一下。

int val[MAXN],ls[MAXN],rs[MAXN],father[MAXN],orsum[MAXN],size[MAXN],rev[MAXN];
inline bool isroot(int u) {
return ls[father[u]]!=u && rs[father[u]]!=u;
}
inline void pushup(int u) {
orsum[u]=val[u]^orsum[ls[u]]^orsum[rs[u]];
size[u]=+size[ls[u]]+size[rs[u]];
}
inline void reverse(int u) {
if(u) {
swap(ls[u],rs[u]);
rev[u]^=;
}
}
inline void pushdown(int u) {
if(rev[u]) {
reverse(ls[u]);
reverse(rs[u]);
rev[u]^=;
}
}
inline void rotate(int S) { //风格比较清奇,凑活看吧qaq
int u=father[S]; if(father[u]) { if(ls[father[u]]==u) ls[father[u]]=S; else if(rs[father[u]]==u) rs[father[u]]=S; }
father[S]=father[u]; father[u]=S;
if(ls[u]==S) {
if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u;
}
else {
if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u;
}
pushup(u); pushup(S);
}
void pushall(int u) {
if(!isroot(u)) {
pushall(father[u]);
pushdown(father[u]);
}
}
inline void Splay(int u) {
pushall(u); pushdown(u); //执行操作之前先pushdown.当然也可以手写栈替代系统栈
while(!isroot(u)) {
if(isroot(father[u]))
rotate(u);
else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u]))
rotate(father[u]), rotate(u);
else
rotate(u), rotate(u);
}
}

三、模板题目

洛谷3690 【模板】Link Cut Tree

给定n个点以及每个点的权值,要你处理接下来的m个操作。操作有4种。操作从0到3编号。点从1到n编号。

0:后接两个整数(x,y),代表询问从x到y的路径上的点的权值的xor和。保证x到y是联通的。

1:后接两个整数(x,y),代表连接x到y,若x到y已经联通则无需连接。

2:后接两个整数(x,y),代表删除边(x,y),不保证边(x,y)存在。

3:后接两个整数(x,y),代表将点x上的权值变成y。

这是一道裸模板题(其实也没有什么更复杂的操作了,大概就是这么回事)

/*
716ms/4835K
多维护了一个size
*/
#include <iostream>
#include <cstdio> using namespace std; const int MAXN=;
int val[MAXN],ls[MAXN],rs[MAXN],father[MAXN],orsum[MAXN],size[MAXN],rev[MAXN];
inline bool isroot(int u) {
return ls[father[u]]!=u && rs[father[u]]!=u;
}
inline void pushup(int u) {
orsum[u]=val[u]^orsum[ls[u]]^orsum[rs[u]];
size[u]=+size[ls[u]]+size[rs[u]];
}
inline void reverse(int u) {
if(u) {
swap(ls[u],rs[u]);
rev[u]^=;
}
}
inline void pushdown(int u) {
if(rev[u]) {
reverse(ls[u]);
reverse(rs[u]);
rev[u]^=;
}
}
inline void rotate(int S) {
int u=father[S]; if(father[u]) { if(ls[father[u]]==u) ls[father[u]]=S; else if(rs[father[u]]==u) rs[father[u]]=S; }
father[S]=father[u]; father[u]=S;
if(ls[u]==S) {
if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u;
}
else {
if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u;
}
pushup(u); pushup(S);
}
void pushall(int u) {
if(!isroot(u)) {
pushall(father[u]);
pushdown(father[u]);
}
}
inline void Splay(int u) {
pushall(u); pushdown(u);
while(!isroot(u)) {
if(isroot(father[u]))
rotate(u);
else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u]))
rotate(father[u]), rotate(u);
else
rotate(u), rotate(u);
}
}
inline void access(int u) {
for(int v=;u;v=u,u=father[u])
Splay(u), rs[u]=v, pushup(u);
}
inline void makeroot(int u) {
access(u); Splay(u); reverse(u);
}
inline int findroot(int u) {
access(u); Splay(u); pushdown(u);
while(ls[u]) pushdown(u),u=ls[u];
return u;
}
inline void split(int u,int v) {
makeroot(u); access(v); Splay(v);
}
inline void link(int u,int v) {
makeroot(u);
if(findroot(v)!=u)
father[u]=v;
}
inline void cut(int u,int v) {
makeroot(u);
if(findroot(v)==u && father[u]==v && rs[u]==)
father[u]=ls[v]=,pushup(v);
}
inline int read() {
char ch=getchar(); int ret=; while(ch<'' || ch>'') ch=getchar();
while(ch>='' && ch<='') ret=ret*+(ch-''), ch=getchar(); return ret;
}
int main() {
int n=read(),m=read(),opt,x,y;
for(int i=;i<=n;i++)
val[i]=orsum[i]=read(),size[i]=;
while(m--) {
opt=read(); x=read(); y=read();
if(opt==)
split(x,y), printf("%d %d\n",orsum[y],size[y]);
else if(opt==)
link(x,y);
else if(opt==)
cut(x,y);
else access(x), Splay(x), val[x]=y, pushup(x);
}
}

洛谷3203 BZOJ2002 [HNOI2010]弹飞绵羊

某天,Lostmonkey发明了一种超级弹力装置,为了在他的绵羊朋友面前显摆,他邀请小绵羊一起玩个游戏。游戏一开始,Lostmonkey在地上沿着一条直线摆上n个装置,每个装置设定初始弹力系数ki,当绵羊达到第i个装置时,它会往后弹ki步,达到第i+ki个装置,若不存在第i+ki个装置,则绵羊被弹飞。绵羊想知道当它从第i个装置起步时,被弹几次后会被弹飞。为了使得游戏更有趣,Lostmonkey可以修改某个弹力装置的弹力系数,任何时候弹力系数均为正整数。

第一行包含一个整数n,表示地上有n个装置,装置的编号从0到n-1。

接下来一行有n个正整数,依次为那n个装置的初始弹力系数。

第三行有一个正整数m,接下来m行每行至少有两个数i、j,若i=1,你要输出从j出发被弹几次后被弹飞,若i=2则还会再输入一个正整数k,表示第j个弹力装置的系数被修改成k。

数据范围:n<=200000,m<=100000。

这道题非常容易转化成LCT的形式。第i个点与第i+k个点之间连边,每一次修改只需要cut&link一遍,被弹的次数可以用size来维护;

建一个编号为n+1的节点代表被弹飞的区域,把所有i+k>n的边连到这里,每次查询size[j~n+1]-1即可。

#include <iostream>
#include <cstdio> using namespace std; const int MAXN=;
int val[MAXN],ls[MAXN],rs[MAXN],father[MAXN],size[MAXN],rev[MAXN];
inline bool isroot(int u) {
return ls[father[u]]!=u && rs[father[u]]!=u;
}
inline void pushup(int u) {
size[u]=+size[ls[u]]+size[rs[u]];
}
inline void reverse(int u) {
if(u) {
swap(ls[u],rs[u]);
rev[u]^=;
}
}
inline void pushdown(int u) {
if(rev[u]) {
reverse(ls[u]);
reverse(rs[u]);
rev[u]^=;
}
}
inline void rotate(int S) {
int u=father[S]; if(father[u]) { if(ls[father[u]]==u) ls[father[u]]=S; else if(rs[father[u]]==u) rs[father[u]]=S; }
father[S]=father[u]; father[u]=S;
if(ls[u]==S) {
if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u;
}
else {
if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u;
}
pushup(u); pushup(S);
}
void pushall(int u) {
if(!isroot(u)) {
pushall(father[u]);
pushdown(father[u]);
}
}
inline void Splay(int u) {
pushall(u); pushdown(u);
while(!isroot(u)) {
if(isroot(father[u]))
rotate(u);
else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u]))
rotate(father[u]), rotate(u);
else
rotate(u), rotate(u);
}
}
inline void access(int u) {
for(int v=;u;v=u,u=father[u])
Splay(u), rs[u]=v, pushup(u);
}
inline void makeroot(int u) {
access(u); Splay(u); reverse(u);
}
inline int findroot(int u) {
access(u); Splay(u); pushdown(u);
while(ls[u]) pushdown(u),u=ls[u];
return u;
}
inline void split(int u,int v) {
makeroot(u); access(v); Splay(v);
}
inline void link(int u,int v) {
makeroot(u);
if(findroot(v)!=u)
father[u]=v;
}
inline void cut(int u,int v) {
makeroot(u);
if(findroot(v)==u && father[u]==v && rs[u]==)
father[u]=ls[v]=,pushup(v);
}
inline int read() {
char ch=getchar(); int ret=; while(ch<'' || ch>'') ch=getchar();
while(ch>='' && ch<='') ret=ret*+(ch-''), ch=getchar(); return ret;
}
int kk[MAXN];
int main() {
int n=read(),m,opt,x,y;
for(int i=;i<=+n;i++)
size[i]=;
for(int i=;i<=n;i++) {
kk[i]=read();
if(i+kk[i]<=n)
link(i,i+kk[i]);
else link(i,n+);
}
m=read();
while(m--) {
opt=read(); x=read(); x++;
if(opt==)
split(x,n+), printf("%d\n",size[n+]-);
else {
y=read();
if(y!=kk[x])
cut(x,(x+kk[x]>n?n+:x+kk[x])), kk[x]=y, link(x,(x+kk[x]>n?n+:x+kk[x]));
}
}
}

洛谷1501 [国家集训队] Tree II

一棵n个点的树,每个点的初始权值为1。对于这棵树有q个操作,每个操作为以下四种操作之一:

  • + u v c:将u到v的路径上的点的权值都加上自然数c;

  • - u1 v1 u2 v2:将树中原有的边(u1,v1)删除,加入一条新边(u2,v2),保证操作完之后仍然是一棵树;

  • * u v c:将u到v的路径上的点的权值都乘上自然数c;

  • / u v:询问u到v的路径上的点的权值和,求出答案对于51061的余数。

数据范围:1<=n,q<=10^5,0<=c<=10^4。

LCT本身很简单,甚至都不需要判连通性

这道题的难点仅在于pushdown的处理。

细节:需要注意51657^2会爆int,需要用unsigned

还有:千万注意区间乘操作可以乘0!血的教训qaq

#include <iostream>
#include <cstdio> using namespace std; const unsigned int MAXN=;
const unsigned int PYZ=; unsigned int val[MAXN],father[MAXN],ls[MAXN],rs[MAXN],rev[MAXN],sum[MAXN];
unsigned int taga[MAXN],tagc[MAXN],size[MAXN]; inline bool isroot(int u) {
return ls[father[u]]!=u && rs[father[u]]!=u;
}
inline void pushup(int u) {
sum[u]=val[u]+sum[ls[u]]+sum[rs[u]]; sum[u]%=PYZ;
size[u]=+size[ls[u]]+size[rs[u]];
}
inline void reverse(int u) {
if(u) {
rev[u]^=;
swap(ls[u],rs[u]);
}
}
inline void pusha(int u,int c) {
taga[u]+=c, taga[u]%=PYZ;
val[u]+=c, val[u]%=PYZ;
sum[u]+=(c*size[u])%PYZ, sum[u]%=PYZ;
}
inline void pushc(int u,int c) {
taga[u]*=c, taga[u]%=PYZ;
val[u]*=c, val[u]%=PYZ;
sum[u]*=c, sum[u]%=PYZ;
tagc[u]*=c, tagc[u]%=PYZ;
}
inline void pushdown(int u) {
if(rev[u]) {
reverse(ls[u]);
reverse(rs[u]);
rev[u]=;
}
if(tagc[u]!=)
pushc(ls[u],tagc[u]), pushc(rs[u],tagc[u]), tagc[u]=;
if(taga[u])
pusha(ls[u],taga[u]), pusha(rs[u],taga[u]), taga[u]=;
}
inline void rotate(int S) {
int u=father[S]; if(father[u]) { if(ls[father[u]]==u) ls[father[u]]=S; else if(rs[father[u]]==u) rs[father[u]]=S; }
father[S]=father[u]; father[u]=S;
if(ls[u]==S) {
if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u;
}
else {
if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u;
}
pushup(u); pushup(S);
}
void pushall(int u) {
if(!isroot(u)) {
pushall(father[u]);
pushdown(father[u]);
}
}
inline void Splay(int u) {
pushall(u); pushdown(u);
while(!isroot(u)) {
if(isroot(father[u]))
rotate(u);
else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u]))
rotate(father[u]), rotate(u);
else rotate(u), rotate(u);
}
pushup(u);
}
inline int kth(int u,int k) {
if(k<=size[ls[k]])
return kth(ls[u],k);
else if(k==size[ls[k]]+)
return u;
else return kth(rs[u],k-size[ls[k]]-);
}
inline void access(int u) {
for(int v=;u;v=u,u=father[u])
Splay(u), rs[u]=v, pushup(u);
}
inline void makeroot(int u) {
access(u); Splay(u); reverse(u);
}
inline int findroot(int u) {
access(u); Splay(u); while(ls[u]) pushdown(u), u=ls[u]; return u;
}
inline void split(int u,int v) {
makeroot(u); access(v); Splay(v);
}
inline void link(int u,int v) {
makeroot(u); father[u]=v;
}
inline void cut(int u,int v) {
split(u,v); ls[v]=father[u]=, pushup(v);
}
inline int read() {
char ch=getchar(); while(ch!='+' && ch!='-' && ch!='*' && ch!='/') ch=getchar();
getchar(); if(ch=='+') return ; if(ch=='-') return ; if(ch=='*') return ; return ;
}
int main() {
int n,q,op,x,y,z; scanf("%d%d",&n,&q);
for(int i=;i<=n;i++) {
size[i]=tagc[i]=val[i]=sum[i]=;
taga[i]=;
}
for(int i=;i<n;i++)
scanf("%d%d",&x,&y) ,link(x,y);
while(q--) {
op=read();
if(op==) {
scanf("%d%d%d",&x,&y,&z);
split(x,y); pusha(y,z);
}
else if(op==) {
scanf("%d%d",&x,&y); cut(x,y);
scanf("%d%d",&x,&y); link(x,y);
}
else if(op==) {
scanf("%d%d%d",&x,&y,&z);
split(x,y); pushc(y,z);
}
else {
scanf("%d%d",&x,&y);
split(x,y); printf("%d\n",(int)sum[y]%);
}
}
}

洛谷2173 BZOJ2816 [ZJOI2012] 网络

有一个无向图G,每个点有个权值,每条边有一个颜色。这个无向图满足以下两个条件:

  1. 对于任意节点连出去的边中,相同颜色的边不超过两条。

  2. 图中不存在同色的环,同色的环指相同颜色的边构成的环。

在这个图上,你要支持以下三种操作:

  1. 修改一个节点的权值。

  2. 修改一条边的颜色。

  3. 查询由颜色c的边构成的图中,所有可能在节点u到节点v之间的简单路径上的节点的权值的最大值。

输出格式

  1. 对于修改节点权值操作,不需要输出信息。

  2. 对于修改边的颜色操作,按以下几类输出:

a) 若不存在连接节点u和节点v的边,输出“No such edge.”。

b) 若修改后不满足条件1,不修改边的颜色,并输出“Error 1.”。

c) 若修改后不满足条件2,不修改边的颜色,并输出“Error 2.”。

d) 其他情况,成功修改边的颜色,并输出“Success.”。

输出满足条件的第一条信息即可,即若同时满足b和c,则只需要输出“Error 1.”。

  1. 对于查询操作,直接输出一个整数。

数据范围:N ≤ 10000,M ≤ 100000,C ≤ 10,K ≤ 100000。

C最大只有10。容易看出,只要把每一个颜色对应一棵LCT,对于改颜色操作多判一判有没有连边,剩下的操作就和裸题差不多了。

把整个数据结构封装一下,搞定。

P.S.记得特判,新边颜色可能会与旧边颜色相等!

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring> using namespace std; const int MAXN=;
const int INF=; struct tree {
int father[MAXN],ls[MAXN],rs[MAXN];
int val[MAXN],sum[MAXN],rev[MAXN],du[MAXN];
inline bool isroot(int u) {
return ls[father[u]]!=u && rs[father[u]]!=u;
}
inline void pushup(int u) {
sum[u]=max(max(val[u],ls[u]?sum[ls[u]]:-INF),rs[u]?sum[rs[u]]:-INF);
}
inline void reverse(int u) {
if(u)
rev[u]^=, swap(ls[u],rs[u]);
}
inline void pushdown(int u) {
if(rev[u])
reverse(ls[u]), reverse(rs[u]),
rev[u]=;
}
inline void rotate(int S) {
int u=father[S];
if(father[u]) {
if(ls[father[u]]==u)
ls[father[u]]=S;
else if(rs[father[u]]==u)
rs[father[u]]=S;
}
father[S]=father[u]; father[u]=S;
if(ls[u]==S) {
if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u;
}
else {
if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u;
}
pushup(u); pushup(S);
}
void pushall(int u) {
if(!isroot(u))
pushall(father[u]), pushdown(father[u]);
}
inline void Splay(int u) {
pushall(u); pushdown(u);
while(!isroot(u)) {
if(isroot(father[u]))
rotate(u);
else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u]))
rotate(father[u]), rotate(u);
else rotate(u), rotate(u);
}
}
inline void access(int u) {
for(int v=;u;v=u,u=father[u])
Splay(u), rs[u]=v, pushup(u);
}
inline void makeroot(int u) {
access(u); Splay(u); reverse(u);
}
inline void split(int u,int v) {
makeroot(u); access(v); Splay(v);
}
inline int findroot(int u) {
access(u); Splay(u);
while(ls[u]) u=ls[u], pushdown(u); return u;
}
inline bool link(int u,int v) {
if(du[u]> || du[v]>) {
printf("Error 1.\n");
return false;
}
makeroot(u);
if(findroot(v)!=u) {
father[u]=v;
du[u]++, du[v]++;
return true;
}
printf("Error 2.\n");
return false;
}
inline void cut(int u,int v) {
makeroot(u);
if(findroot(v)==u && ls[v]==u && rs[u]==) {
father[u]=ls[v]=, pushup(v);
du[u]--, du[v]--;
}
}
}lct[]; int main() {
int n,m,c,k,dat,opt,x,y,z;
scanf("%d%d%d%d",&n,&m,&c,&k);
for(int i=;i<=n;i++) {
scanf("%d",&dat);
for(int o=;o<=c;o++)
lct[o].val[i]=lct[o].sum[i]=dat;
}
for(int i=;i<=m;i++) {
scanf("%d%d%d",&x,&y,&z);
lct[z].link(x,y);
}
while(k--) {
scanf("%d",&opt);
if(opt==) {
scanf("%d%d",&x,&y);
for(int o=;o<=c;o++) {
lct[o].access(x), lct[o].Splay(x);
lct[o].val[x]=y, lct[o].pushup(x);
}
}
else if(opt==) {
bool _f,flag=false; int des;
scanf("%d%d%d",&x,&y,&z);
lct[z].makeroot(x);
if(x==lct[z].findroot(y) && lct[z].ls[y]==x && lct[z].rs[x]==) {
printf("Success.\n");
continue;
}
for(int o=;o<=c;o++) {
if(o==z)
continue;
lct[o].makeroot(x);
if(x==lct[o].findroot(y) && lct[o].ls[y]==x && lct[o].rs[x]==)
flag=true, des=o;
}
if(flag==false) {
printf("No such edge.\n");
continue;
}
if(lct[z].link(x,y)) {
lct[des].cut(x,y);
printf("Success.\n");
}
}
else {
scanf("%d%d%d",&x,&y,&z);
lct[x].makeroot(y);
if(lct[x].findroot(z)==y) {
lct[x].split(y,z);
printf("%d\n",lct[x].sum[z]);
}
else printf("-1\n");
}
}
}

四、维护子树信息——LCT扩展

LCT的基本应用已经详细地阐述清楚了。

可是当遇到某些特别的与树相关的问题时,我们发现不能用LCT来解决问题。

这个时候就需要一些更加高级的技巧了。

洛谷4219 BZOJ4530 [BJOI2014]大融合

给定n个点,进行Q次操作,操作有两种:

A x y :在x,y之间连一条边,保证x,y不连通

Q x y :求通过边(x,y)的简单路径数量,保证存在边(x,y)

N,Q<=10^5

这是LCT最强大的一个功能:对子树信息的维护。

我们可以发现,事实上LCT臃肿的代码里,真正涉及到轻重边切换的地方很少,事实上只有access和link两个地方。

以此题为例,我们可以开一个sizex数组表示其虚子树的大小之和,我们就可以轻松水过这道题。

#include <iostream>
#include <cstdio> using namespace std; const int MAXN=; int size[MAXN],sizex[MAXN],ls[MAXN],rs[MAXN],father[MAXN],rev[MAXN]; inline bool isroot(int u) {
return ls[father[u]]!=u && rs[father[u]]!=u;
}
inline void reverse(int u) {
if(u)
swap(ls[u],rs[u]), rev[u]^=;
}
inline void pushdown(int u) {
if(rev[u])
reverse(ls[u]), reverse(rs[u]), rev[u]=;
}
inline void pushup(int u) {
size[u]=+size[ls[u]]+sizex[ls[u]]+size[rs[u]]+sizex[rs[u]];
}
inline void rotate(int S) {
int u=father[S]; if(father[u]) {
if(ls[father[u]]==u) ls[father[u]]=S;
else if(rs[father[u]]==u) rs[father[u]]=S;
}
father[S]=father[u]; father[u]=S;
if(ls[u]==S) {
if(rs[S]) father[rs[S]]=u; ls[u]=rs[S]; rs[S]=u;
}
else {
if(ls[S]) father[ls[S]]=u; rs[u]=ls[S]; ls[S]=u;
}
pushup(u); pushup(S);
}
void pushall(int u) {
if(!isroot(u))
pushall(father[u]), pushdown(father[u]);
}
inline void Splay(int u) {
pushall(u); pushdown(u);
while(!isroot(u)) {
if(isroot(father[u]))
rotate(u);
else if((ls[father[u]]==u) == (ls[father[father[u]]]==father[u]))
rotate(father[u]), rotate(u);
else rotate(u), rotate(u);
}
}
inline void access(int u) {
for(int v=;u;v=u,u=father[u]) {
Splay(u);
if(rs[u])
sizex[u]+=size[rs[u]]+sizex[rs[u]];
rs[u]=v;
if(v)
sizex[u]-=size[v]+sizex[v];
pushup(u);
}
}
inline void makeroot(int u) {
access(u); Splay(u); reverse(u);
}
inline void split(int u,int v) {
makeroot(u); access(v); Splay(v);
}
inline void link(int u,int v) {
split(u,v); father[u]=v; sizex[v]+=size[u]+sizex[u];
}
inline char read() {
char ch=getchar(); while(ch!='A' && ch!='Q') ch=getchar(); return ch;
}
int main() {
int n,q,x,y; char ch; scanf("%d%d",&n,&q);
for(int i=;i<=n;i++)
size[i]=;
while(q--) {
ch=read();
if(ch=='A') {
scanf("%d%d",&x,&y);
link(x,y);
}
else {
scanf("%d%d",&x,&y);
split(x,y);
printf("%lld\n",1ll*(size[x]+sizex[x])*(size[y]+sizex[y]-size[x]-sizex[x]));
}
}
}