动态树(LCT)
动态树是动态维护树与森林的信息的结构。可以在一个 $\log $ 的时间内快速动态维护各种操作,包括但不限于:
- 删除边
- 添加边
- 换根
- 求树上链的信息,如异或和,最大值等
- 修改树上某个点的值
模板
LCT 的全称是 Link-Cut-Tree,即可以连边删边的树。其动态维护的特性是基于 splay 这种平衡树以及实链剖分来维护的。
实链剖分是指将树上的边划分为实链和虚链。在 LCT 中,所有由实链相连的点都在同一个 splay 中,而由虚链连接的点都不在同一个 splay 中。
虚链在形式上有“认父不认子”的特点。如果我们要走虚链,我们只能从儿子走向其通过虚链连接的父亲,而不能从父亲走向其通过虚链连接的儿子。
对于森林中的每棵树,其节点通过实链剖分分别被划分到多个 splay 中。每个 splay 的一些重要性质如下:
- 中序遍历 splay 中的点,每个点在原树上的深度严格递增。这意味着任意一颗 splay 中没有在原树中深度相同的两个点。注意,这里是中序遍历 splay。也就是说 splay 的形态很有可能与原树的形态有巨大的不同。
- 每个节点在且仅在一颗 splay 中。
- 所有的实链都是 splay 内部的边,所有的虚链都是 splay 之间连接的边。正如上面所说,只有下层的 splay 才能够通过虚链向上层的 splay 走。
(偷一些图。from FlashHu)
假设我们将一棵树的边划分为如下形态:

实链和虚链的划分就如图所示。
根据上面的定义和性质,我们将实链连接在一起的点建成一颗 splay。

一个绿色框里的点组成一颗 splay。可以发现,其形态与原树几乎完全不同,连边非常奇怪。注意虚边是从原树上的父亲连向对应的 splay 的根节点。
如果没看懂,注意回顾上面的定义和性质。
access
个人在在代码里的函数名简写为 acc。
这是整个 LCT 操作的精髓。其含义是将某个点 \(u\) 到 \(u\) 所在原树的根的链上的所有点全部放进一个 splay 中,同时除了这条链上的所有点其他所有点都不在这个 splay 中。
假设我们要操作上图。下面是我们 acc(N) 这个点的情况。

也就是到根的这条链全部变成实链,其余与这条链相连的链全部变成虚链。注意单独一个点也是可以组成一颗 splay。
实际上怎么做呢?我们考虑每次将我们要换的这个点旋转到 splay 顶,然后直接令其虚链或者实链指向的父亲的儿子指向自己即可。由于当前点在原树上的深度一定大于其父亲,因此我们可以直接将其连向父亲的右儿子。
void acc(int u){for(int v=0;u;v=u,u=fa[v])splay(u),ch[u][1]=v,push_up(u);}
makeroot
简写为 makert。
作用是换根。注意,这个“换根”操作是真正意义上的换根。包括深度等等信息都是新的而不是原来的。
在操作层面这个操作实际很简单。我们先将路径打通。可以发现这个时候当前点一定是所在 splay 的最深的节点。因此当我们将其旋转到这个 splay 的顶的时候,所有的节点都一定在其左子树内。
因此,我们可以直接交换左右子树,这样在就使得当前点从最深点直接变为最浅点(中序遍历的第一个点)。
但是我们需要维护一个反转标记,因为翻转 splay 需要翻转每个节点的左右子树。
void makert(int u){acc(u);splay(u);rev(u);}//rev(u) 就是翻转 u 这个节点的左右子树
findroot
简写为 findrt。
顾名思义,找到当前所在树的树根。
同样是比较暴力的思想,我们直接 acc 这个点,然后将这个点旋转到顶,然后一直向左子树跳就一定可以跳到当前树根。不过需要注意翻转标记的问题,需要一边查询一遍下放标记。
最后还要将树根转到顶,说是保证复杂度。但为什么我也不知道。
int findrt(int u){for(acc(u),splay(u);ch[u][sign[u]];push_down(u),u=ch[u][0]);splay(u);return u;}//sign 是翻转标记
split
不简写。
作用是将 \(u,v\) 的链拉出来单独建一颗 splay。由于我们有很强的换根操作,因此我们直接将 \(u\) 变成根然后 acc(v) 即可。注意做完之后要将 \(v\) 转到 splay 的顶。
void split(int u,int v){makert(u);acc(v);splay(v);}
link
简写为 lnk。注意 link 是关键字,可能在高版本无法通过编译。
这个就是 LCT 名字中的连边操作。我们只需要暴力的将两个点分别转到其树的根然后相连即可。注意这个连的是虚链。
当然,如果不保证连边合法还需要判断一下是否可以连边。
void lnk(int u,int v){makert(u);if(findrt(v)!=u)makert(v),fa[v]=u;}
cut
不简写。
这个就是删边操作。可以发现如果 \(u,v\) 之间确实有边,那么将 \(u,v\) 这条链拉出来后由于有 split(v), \(u\) 一定是 \(v\) 的左子树(注意 split 操作会使 \(u\) 变成树根,这时 \(u\) 深度一定小于 \(v\),但是这个时候所在的 splay 的顶是 \(v\))。直接将实边断掉即可。
但是如果可能不合法,那就需要多判断一些东西。直接判断这条虚边是否存在即可。
void cut(int u,int v){split(u,v);if(fa[u]==v&&!ch[v][1]) fa[u]=ch[v][0]=0,push_up(v);}
query
不简写。
查询链的信息。
直接将 \(u,v\) 这条链拉出来即可。注意 split 后 \(v\) 是这颗 splay 的顶。
void query(int u,int v){split(u,v);cout<<sum[v]<<'\n';}//sum 是维护的信息,在 push_up 这个函数中维护。可以在后面的代码中统一看
modify
不简写。
修改某个点的信息。
由于我们在 splay 上的信息是从实链所连的点合并而来的,因此我们可以直接将其转到 splay 顶使得其不对任何其他点产生影响。
void modify(int u,int w){splay(u);val[u]=w;push_up(u);}
其他
就是 splay 的附带操作以及维护信息的 push_up,下放标记的 push_down。
注意理解一下,有很多地方与传统的 splay 不同。
#define ls (ch[u][0])
#define rs (ch[u][1])
void push_up(int u){sum[u]=sum[ls]^sum[rs]^val[u];} //维护信息。这里是异或和
int id(int u){return ch[fa[u]][1]==u;} //找到当前节点在父亲的左儿子还是右耳子
int get(int u){return ch[fa[u]][id(u)]==u;} //判断是否是当前 splay 的根
void rev(int u){swap(ls,rs);sign[u]^=1;} //区间翻转
void push_down(int u){if(sign[u]){rev(ls),rev(rs);sign[u]=0;}}//下放翻转标记
void pushall(int u){if(get(u))pushall(fa[u]);push_down(u);} //下放一个点到 splay 的根的所有翻转标记
void trs(int x,int y,int k){fa[x]=y;ch[y][k]=x;} //在 splay 里连边
void rotate(int x){ //单次旋转
int y=fa[x],z=fa[y],k=id(x),u=ch[x][!k];
if(get(y))ch[z][id(y)]=x;fa[x]=z;
trs(u,y,k),trs(y,x,!k);push_up(y),push_up(x);
}
void splay(int u){for(pushall(u);get(u);rotate(u))if(get(fa[u]))rotate(id(u)==id(fa[u])?fa[u]:u);}//旋转到根
code
封装了一下。注意在实现 LCT 的时候压行的必要性。否则看起来会比较麻烦。
namespace LCT{
int sum[N],ch[N][2],fa[N],sign[N];
#define ls (ch[u][0])
#define rs (ch[u][1])
void push_up(int u){sum[u]=sum[ls]^sum[rs]^val[u];}
int id(int u){return ch[fa[u]][1]==u;}
int get(int u){return ch[fa[u]][id(u)]==u;}
void rev(int u){swap(ls,rs);sign[u]^=1;}
void push_down(int u){if(sign[u]){rev(ls),rev(rs);sign[u]=0;}}
void pushall(int u){if(get(u))pushall(fa[u]);push_down(u);}
void trs(int x,int y,int k){fa[x]=y;ch[y][k]=x;}
void rotate(int x){
int y=fa[x],z=fa[y],k=id(x),u=ch[x][!k];
if(get(y))ch[z][id(y)]=x;fa[x]=z;
trs(u,y,k),trs(y,x,!k);push_up(y),push_up(x);
}
void splay(int u){for(pushall(u);get(u);rotate(u))if(get(fa[u]))rotate(id(u)==id(fa[u])?fa[u]:u);}
void acc(int u){for(int v=0;u;v=u,u=fa[v])splay(u),ch[u][1]=v,push_up(u);}
void makert(int u){acc(u);splay(u);rev(u);}
int findrt(int u){for(acc(u),splay(u);ch[u][sign[u]];push_down(u),u=ch[u][0]);splay(u);return u;}
void split(int u,int v){makert(u);acc(v);splay(v);}
void lnk(int u,int v){makert(u);if(findrt(v)!=u)makert(v),fa[v]=u;}
void cut(int u,int v){split(u,v);if(fa[u]==v&&!ch[v][1]) fa[u]=ch[v][0]=0,push_up(v);}
void query(int u,int v){split(u,v);cout<<sum[v]<<'\n';}
void modify(int u,int w){splay(u);val[u]=w;push_up(u);}
}
另给出机房大佬 nkp 的优雅实现。感觉比我的好看很多。
点击查看代码
namespace LCT{
#define ls ch[x][0]
#define rs ch[x][1]
inline bool id(int x){return x == ch[fa[x]][1];}
inline bool get(int x){return x == ch[fa[x]][id(x)];}
inline void rvs(int x){swap(ls, rs); rev[x] ^= 1;}
inline void upd(int x){s[x] = s[ls] ^ s[rs] ^ val[x];}
inline void pd(int x){if(rev[x])rvs(ls), rvs(rs); rev[x] = 0;}
inline void pu(int x){if(get(x))pu(fa[x]); pd(x);}
inline void trs(int x, int y, bool k){if(x)fa[x] = y; ch[y][k] = x;}
inline void rot(int x){
int y = fa[x], z = fa[y], k = id(x), u = ch[x][! k];
if(get(y))ch[z][id(y)] = x; fa[x] = z;
trs(u, y, k); trs(y, x, ! k); upd(y); upd(x);
}
inline void spl(int x){for(pu(x); get(x); rot(x))if(get(fa[x]))rot(id(x) == id(fa[x]) ? fa[x] : x);}
inline void acc(int x){for(int z = x, y = 0; z; z = fa[y = z])spl(z), ch[z][1] = y, upd(z); spl(x);}
inline int fdrt(int x){for(acc(x); ch[x][rev[x]];pd(x), x = ch[x][0]); return spl(x), x;}
inline void mkrt(int x){acc(x); rvs(x);}
inline void spt(int x, int y){mkrt(x); acc(y);}
inline void link(int x, int y){if(mkrt(x), fdrt(y) != x)mkrt(y),fa[y] = x;}
inline void cut(int x, int y){spt(x, y);if(fa[x] == y and ! ch[y][1])fa[x] = ch[y][0] = 0, upd(y);}
#undef ls
#undef rs
}
复杂度
单次操作的时间复杂度达到了非常优秀的 \(\log n\)。并不太会证,主要是不知道 splay 的复杂度是如何保证的。(不会 splay 导致的)
空间复杂度显然线性。
应用
这个东西看起来就非常强,可以干很多事情。
不过一般来讲 LCT 都是维护树上链的信息,子树信息由于虚边的存在就不好统计。一般子树信息还是用树剖比较正常。
P3690 【模板】动态树(LCT)
实际上就是上面的代码。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
int val[N];
namespace LCT{
int sum[N],ch[N][2],fa[N],sign[N];
#define ls (ch[u][0])
#define rs (ch[u][1])
void push_up(int u){sum[u]=sum[ls]^sum[rs]^val[u];}
int id(int u){return ch[fa[u]][1]==u;}
int get(int u){return ch[fa[u]][id(u)]==u;}
void rev(int u){swap(ls,rs);sign[u]^=1;}
void push_down(int u){if(sign[u]){rev(ls),rev(rs);sign[u]=0;}}
void pushall(int u){if(get(u))pushall(fa[u]);push_down(u);}
void trs(int x,int y,int k){fa[x]=y;ch[y][k]=x;}
void rotate(int x){
int y=fa[x],z=fa[y],k=id(x),u=ch[x][!k];
if(get(y))ch[z][id(y)]=x;fa[x]=z;
trs(u,y,k),trs(y,x,!k);push_up(y),push_up(x);
}
void splay(int u){for(pushall(u);get(u);rotate(u))if(get(fa[u]))rotate(id(u)==id(fa[u])?fa[u]:u);}
void acc(int u){for(int v=0;u;v=u,u=fa[v])splay(u),ch[u][1]=v,push_up(u);}
void makert(int u){acc(u);splay(u);rev(u);}
int findrt(int u){for(acc(u),splay(u);ch[u][sign[u]];push_down(u),u=ch[u][0]);splay(u);return u;}
void split(int u,int v){makert(u);acc(v);splay(v);}
void lnk(int u,int v){makert(u);if(findrt(v)!=u)makert(v),fa[v]=u;}
void cut(int u,int v){split(u,v);if(fa[u]==v&&!ch[v][1]) fa[u]=ch[v][0]=0,push_up(v);}
void query(int u,int v){split(u,v);cout<<sum[v]<<'\n';}
void modify(int u,int w){splay(u);val[u]=w;push_up(u);}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++) cin>>val[i];
for(int i=1,op,x,y;i<=m;i++){
cin>>op>>x>>y;
if(op==0) LCT::query(x,y);
if(op==1) LCT::lnk(x,y);
if(op==2) LCT::cut(x,y);
if(op==3) LCT::modify(x,y);
}
return 0;
}
P4172 [WC2006] 水管局长(动态维护最小生成树)
考虑删去边再去维护最小生成树是不好做的,因此考虑倒过来,一个一个向里面加边维护最小生成树。
先把最终的状态搞出来,求出最后的最小生成树的形态。再考虑向里面加边的时候生成树的形态什么时候回变化。
假设对于新加入一条 \(u\) 到 \(v\) 的边,其权值为 \(w\)。发现如果原本 \(u,v\) 路径上的权值的最大值如果大于 \(w\),那么将这条比较大的边删去再连上这条边一定更优。
找出来这条边删掉再把这条边加入即可。
如果 \(w\) 比路径上最大的边还大,那么可以发现以后一定不会加入这条边了。
code
发现 LCT 没办法维护边权,因此考虑拆边为点做。注意维护 LCT 内的权值的时候存的是边的序号,因为要找到链上最大的是哪条边。
注意在具体实现的时候开了个 map 来记录边权。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define pi pair <int,int>
#define mp make_pair
const int N=1e3+7,M=1e5+7;
int n,m,qq,vis[N+M],ans[M+N],val[M+N];
map <pi,int> p;
struct node{int op,u,v;}que[N+M];
struct edge{int u,v,w,id;}bian[N+M],tmp[N+M];
bool cmp1(edge x,edge y){return x.w<y.w;}
namespace LCT{
int ch[N+M][2],fa[N+M],sign[N+M],num[N+M];
#define ls (ch[u][0])
#define rs (ch[u][1])
void push_up(int u){num[u]=u;if(val[num[ls]]>val[num[u]])num[u]=num[ls];if(val[num[rs]]>val[num[u]])num[u]=num[rs];}
void rev(int u){swap(ls,rs);sign[u]^=1;}
void push_down(int u){if(sign[u])rev(ls),rev(rs),sign[u]=0;}
int id(int u){return u==ch[fa[u]][1];}
int get(int u){return u==ch[fa[u]][id(u)];}
void pushall(int u){if(get(u))pushall(fa[u]);push_down(u);}
void trs(int x,int y,int k){if(y)ch[y][k]=x;fa[x]=y;}
void rot(int x){int y=fa[x],z=fa[y],k=id(x),u=ch[x][!k];if(get(y))ch[z][id(y)]=x;fa[x]=z;trs(u,y,k),trs(y,x,!k);push_up(y),push_up(x);}
void splay(int u){for(pushall(u);get(u);rot(u)){if(get(fa[u]))id(u)==id(fa[u])?rot(fa[u]):rot(u);}}
void acc(int u){for(int v=0;u;v=u,u=fa[v])splay(u),ch[u][1]=v,push_up(u);}
void mkrt(int u){acc(u);splay(u);rev(u);}
int findrt(int u){for(acc(u),splay(u);ch[u][sign[u]];push_down(u),u=ch[u][0]);splay(u);return u;}
void split(int u,int v){mkrt(u);acc(v);splay(v);}
void lnk(int u,int v){mkrt(u);if(findrt(v)!=u)mkrt(v),fa[v]=u;}
void cut(int u,int v){split(u,v);if(fa[u]==v&&!ch[v][1]) fa[u]=ch[v][0]=0,push_up(v);}
}
using namespace LCT;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>qq;
for(int i=1+n,u,v,w;i<=m+n;i++){cin>>u>>v>>w;if(u>v)swap(u,v);p[mp(u,v)]=i;bian[i]=tmp[i]={u,v,w,i},val[i]=w,num[i]=i;}
for(int i=1,op,u,v;i<=qq;i++){cin>>op>>u>>v;if(u>v)swap(u,v);que[i]={op,u,v};if(op==2)vis[p[mp(u,v)]]=1;}
sort(bian+n+1,bian+n+m+1,cmp1);
for(int i=n+1;i<=m+n;i++){
if(vis[bian[i].id])continue;
int u=bian[i].u,v=bian[i].v,id=bian[i].id;
if(findrt(u)==findrt(v))continue;
lnk(u,id),lnk(v,id);
}
for(int i=qq;i>=1;i--){//ans?记得判断是否要删去 “边 ”,即删去某个边点与其相连的两条边
int u=que[i].u,v=que[i].v,op=que[i].op,id=p[mp(u,v)];
if(op==1)split(u,v),ans[i]=val[num[v]];
else {
split(u,v);
if(val[num[v]]>val[id]){
int t=num[v];cut(tmp[t].u,t),cut(tmp[t].v,t);
val[t]=0;lnk(u,id),lnk(v,id);
}
}
}
for(int i=1;i<=qq;i++){if(que[i].op==1)cout<<ans[i]<<'\n';}
return 0;
}
P4180 [BJWC2010] 严格次小生成树
一个重要的结论是严格次小生成树与最小生成树之间的边差距只是删除了最小生成树中的一条边,加入了另外一条边。
这个看起来比较直观,考虑为什么。发现加入一条边以后,一定会形成一个环。由于是最小生成树,因此加入的这条边一定比环上的任意一条边都要大,因此加入两条边的总权值一定比只加入两条边中的任意一条边更劣,也就是说一定不是严格次小生成树。
于是我们首先先将最小生成树建出来,再考虑每一条非树边加入后要删除新生成的环上的哪条边。
假设我们加入的边连接 \(u,v\),我们就仿照上面的操作将其 split 出来,然后查找 \(u,v\) 链上面的最大值,然后计算将其替换掉的贡献。
但是如果这条边与 \(u,v\) 链上的最大值相等呢?为了保证是严格的次小生成树,我们就还要维护一个链的次小值。
然后答案就是对于所有非树边的答案取 \(\min\)。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define pi pair <int,int>
#define mp make_pair
#define ll long long
const int N=1e5+7,M=3e5+7,inf=1e9+7;
int n,m,vis[N+M],ans[M+N],val[M+N],ff[N],tag[N+M];
struct node{int u,v,w,id;}bian[N+M];
bool cmp(node x,node y){return x.w<y.w;}
int find(int x){return x==ff[x]?x:ff[x]=find(ff[x]);}
namespace LCT{
int ch[N+M][2],fa[N+M],sign[N+M],mf[N+M],ms[N+M];
#define ls (ch[u][0])
#define rs (ch[u][1])
void push_up(int u){
if(val[mf[ls]]==val[mf[rs]])mf[u]=mf[ls],ms[u]=(val[ms[ls]]>val[ms[rs]]?ms[ls]:ms[rs]);
if(val[mf[ls]]>val[mf[rs]]) mf[u]=mf[ls],ms[u]=(val[ms[ls]]>val[mf[rs]]?ms[ls]:mf[rs]);
if(val[mf[ls]]<val[mf[rs]]) mf[u]=mf[rs],ms[u]=(val[mf[ls]]>val[ms[rs]]?mf[ls]:ms[rs]);
if(val[u]>val[mf[u]])mf[u]=u;if(val[u]<val[mf[u]]&&val[u]>val[ms[u]])ms[u]=u;
}
void rev(int u){swap(ls,rs);sign[u]^=1;}
void push_down(int u){if(sign[u])rev(ls),rev(rs),sign[u]=0;}
int id(int u){return u==ch[fa[u]][1];}
int get(int u){return u==ch[fa[u]][id(u)];}
void pushall(int u){if(get(u))pushall(fa[u]);push_down(u);}
void trs(int x,int y,int k){if(y)ch[y][k]=x;fa[x]=y;}
void rot(int x){int y=fa[x],z=fa[y],k=id(x),u=ch[x][!k];if(get(y))ch[z][id(y)]=x;fa[x]=z;trs(u,y,k),trs(y,x,!k);push_up(y),push_up(x);}
void splay(int u){for(pushall(u);get(u);rot(u)){if(get(fa[u]))id(u)==id(fa[u])?rot(fa[u]):rot(u);}}
void acc(int u){for(int v=0;u;v=u,u=fa[v])splay(u),ch[u][1]=v,push_up(u);}
void mkrt(int u){acc(u);splay(u);rev(u);}
int findrt(int u){for(acc(u),splay(u);ch[u][sign[u]];push_down(u),u=ch[u][0]);splay(u);return u;}
void split(int u,int v){mkrt(u);acc(v);splay(v);}
void lnk(int u,int v){mkrt(u);if(findrt(v)!=u)mkrt(v),fa[v]=u;}
void cut(int u,int v){split(u,v);if(fa[u]==v&&!ch[v][1]) fa[u]=ch[v][0]=0,push_up(v);}
}
using namespace LCT;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;val[0]=-inf;for(int i=1;i<=n;i++)ff[i]=i,val[i]=-inf,mf[i]=i,ms[i]=0;
for(int i=1+n,u,v,w;i<=m+n;i++){cin>>u>>v>>w;if(u>v)swap(u,v);bian[i]={u,v,w,i},val[i]=w;}
sort(bian+n+1,bian+n+m+1,cmp);
ll tmp=0,ans=1ll*inf*inf;
for(int i=n+1;i<=m+n;i++){
if(vis[bian[i].id])continue;
int u=bian[i].u,v=bian[i].v,id=bian[i].id;
if(find(u)==find(v))continue;
lnk(u,id),lnk(v,id);ff[find(u)]=find(v);tag[i]=1;tmp+=bian[i].w;
}
for(int i=n+1;i<=n+m;i++){
if(tag[i])continue;
int u=bian[i].u,v=bian[i].v,w=bian[i].w;split(u,v);int f=val[mf[v]],s=val[ms[v]];
if(f==w){
if(s==-inf)continue;
ans=min(ans,tmp-s+w);
}
else ans=min(ans,tmp-f+w);
}
cout<<ans;return 0;
return 0;
}

浙公网安备 33010602011771号