更新日志
2025/02/14:开工。
概念
LCT 可以解决动态树下树链信息的维护。
动态树的意思是可以动态割边、连边。
首先我们将讲解整体思路,然后对各个函数依次介绍。
LCT 的均摊复杂度为 \(O(n\log n)\) 的,证明复杂,所以这里全部掠过,保证复杂度的操作都会提出。
实现
维护
由于动态的割边与连边,所以 LCT 维护的实际上是一片森林。
对于每一棵原树,我们对其进行实链剖分。类似于重链剖分,但实链剖分的实链与虚链是我们按需要决定的。
此外,对于每一棵原树,我们维护一棵辅助树。每一棵辅助树由多棵 Splay 组成,每棵 Splay 维护原树中一条实链。
每棵 Splay 中序遍历的顺序是原树中的深度顺序,由浅到深。
对于原树中每一条虚链,我们采取“认父不认子”的维护方式,对于其子节点记录其父节点,但其父节点不计这个子节点。这样可以快速判断这条链是实链还是虚链。
下面我们对每个函数进行详细讲解。
函数
信息维护函数
用于维护树链信息。
在 Splay 上,我们对于每个节点维护其子树信息,那么这棵 Splay 的根节点就维护了这条实链的信息。
pushup pushdown
这里以维护链异或和为例。
同时,我们必须维护一个子树翻转信息,这将会在后面 makeroot
函数中用到。
题目中额外的信息维护,全部写在这里就可以。
void pushup(int x){sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];}void pushdown(int x){if(rev[x]){if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;rev[x]^=1;}}
update
这个函数用于传递一整条链(实际上是当前点到链头)的标记,方便后续对链的操作。
isroot
函数用于判断是否为当前 Splay 根节点,具体实现后面马上就会讲到。
递归更新即可。
void update(int x){if(!isroot(x))update(fa[x]);pushdown(x);}
结构信息函数
isroot
用于判断这个节点是否为当前 Splay 根节点。由于虚链认父不认子,所以我们只需要判定这个节点是否为父节点的子节点之一即可。
#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])
get
用于获取当前节点为父节点的左右子节点。没啥好说的。
#define get(x) (x==son[fa[x]][1])
Splay 函数
由于实链是通过 Splay 维护的,所以会用到一些 Splay 的函数,在这里一并讲解。
rotate
用于把当前节点向上旋转。听起来略微抽象。
借图 OI-Wiki:
\(x\) 为当前节点,\(y\) 为其 Splay 内父节点。上面分别是左右节点的情况。
我们以情况 \(1\) 为例,情况 \(2\) 同理。
首先,为了保证中序遍历顺序不变,我们将 \(y\) 的左子节点设为 \(x\) 的右子节点。如果原来存在左子节点,那么把它变成虚链即可,不用特别处理。
然后,我们把 \(y\) 设作 \(x\) 的右子节点,再把 \(y\) 原来的父节点设作 \(x\) 的父节点。
旋转之后,我们显然要更新节点信息,从下往上更新即可,也就是先 \(y\) 后 \(x\)。
需要注意操作的顺序。
void rotate(int x){int y=fa[x],z=fa[y],k=get(x);if(!isroot(y))son[z][get(y)]=x;son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;son[x][k^1]=y,fa[y]=x,fa[x]=z;pushup(y);pushup(x);}
splay
这个操作用于把一个节点旋转到其所在 Splay 的根节点,同时保证均摊复杂度。
复杂度证明这里暂且略去,如有需要,请自行查询 OI-Wiki。
在旋转操作之前,我们先把一路上的标记全部传递下来,也就是 update
一遍。
zig
当前节点父节点为根的情况,直接向上 rotate
即可。
zig-zig
当前节点父节点是同一侧子节点的情况,具体的:
我们先 rotate(y)
,然后 rotate(x)
即可。
zig-zag
当前节点父节点不是同一侧子节点的情况,具体的:
我们先 rotate(x)
,然后 rotate(y)
即可。
void splay(int x){update(x);for(int f;f=fa[x],!isroot(x);rotate(x))if(!isroot(f))rotate(get(x)==get(f)?f:x);}
核心函数
这两个函数是 LCT 最核心的函数。
access
在原树上将当前点到原树根节点拉出一条实链。
对于我们经过的每一个节点,我们首先将其旋转到其 Splay 的根节点,然后将其右子节点设为上一棵 Splay 的根节点(如果这是第一步,就设为空,因为要单独拉出一条实链,所以要把下面的部分截掉)(旋转到根节点和放右子节点是为了维护中序深度递增)。
由于链发生了变化,别忘了 \(pushup\)。
我们最后走到的节点显然就是原树的根节点。我们这里顺便返回一下辅助树的新根节点,方便 makeroot
操作。
int access(int x){int p;for(p=0;x;p=x,x=fa[x]){splay(x);son[x][1]=p;pushup(x);}return p;}
makeroot
为了方便维护中序深度递增,我们有时候会需要把某一个节点设为原树的根节点,比如连边的时候。
首先我们拉出当前节点到原树根的实链,也就是 access
一遍。
其余节点的相对深度关系都没有变化,因此我们只需要反转这条实链的深度关系即可。
所以我们考虑反转中序顺序,也就是反转每个节点的左右子节点。
void makeroot(int x){x=access(x);swap(son[x][0],son[x][1]);rev[x]^=1;}
常用函数
find
用于查找所在原树的根。
我们将当前节点到原根拉一条实链,然后这条实链中序第一个遍历到的就是根节点。
具体的,我们先把当前节点转到根节点,然后一直往左走就行。
为了维护复杂度,把找到的根节点转到这条实链的根节点。
int find(int x){access(x);splay(x);pushdown(x);while(son[x][0])x=son[x][0],pushdown(x);splay(x);return x;}
split
用于单独拉出 \(x\) 到 \(y\) 的链。前提是两者连通。
我们只需要先把 \(x\) 设为根节点,然后拉通 \(y\) 到根的链即可。
为了方便 cut
操作,我们通常把 \(y\) 转到 Splay 根节点。
void split(int x,int y){makeroot(x);access(y);splay(y);}
link
连接两个节点,前提是两者不连通。
我们令为 \(x\) 连向 \(y\),为了维护深度关系,我们先把 \(x\) 设作原树根节点。
然后,我们把 \(x\) 转到 Splay 的根节点,并连一条向 \(y\) 的虚链即可。
void link(int x,int y){makeroot(x);splay(x);fa[x]=y;}
cut
断开一条边,前提是有这条边。
提前存好边就行。有一个根据性质的判断方法,我不会。
我们先取出连接二者的实链,由于二者有边相连,所以二者在实链上必然相邻。
直接双向断开即可。
void cut(int x,int y){split(x,y);son[y][get(x)]=fa[x]=0;}
change
修改点权操作。为了方便维护子树信息,我们先把要修改的点转到 Splay 根节点,然后直接改,并更新子树信息。
void change(int x,int v){splay(x);val[x]=v;pushup(x);}
模板
以维护异或和为例。
struct LCT{int son[N][2],fa[N];int rev[N];int val[N],sum[N];#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])#define get(x) (x==son[fa[x]][1])void pushup(int x){sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];}void pushdown(int x){if(rev[x]){if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;rev[x]^=1;}}void update(int x){if(!isroot(x))update(fa[x]);pushdown(x);}void rotate(int x){int y=fa[x],z=fa[y],k=get(x);if(!isroot(y))son[z][get(y)]=x;son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;son[x][k^1]=y,fa[y]=x,fa[x]=z;pushup(y);pushup(x);}void splay(int x){update(x);for(int f;f=fa[x],!isroot(x);rotate(x))if(!isroot(f))rotate(get(x)==get(f)?f:x);}int access(int x){int p;for(p=0;x;p=x,x=fa[x]){splay(x);son[x][1]=p;pushup(x);}return p;}void makeroot(int x){x=access(x);swap(son[x][0],son[x][1]);rev[x]^=1;}int find(int x){access(x);splay(x);pushdown(x);while(son[x][0])x=son[x][0],pushdown(x);splay(x);return x;}void link(int x,int y){makeroot(x);splay(x);fa[x]=y;}void split(int x,int y){makeroot(x);access(y);splay(y);}void cut(int x,int y){split(x,y);son[y][get(x)]=fa[x]=0;}void change(int x,int v){splay(x);val[x]=v;pushup(x);}
}lct;
例题
LG3690
代码
#include<bits/stdc++.h>
using namespace std;typedef long long ll;
typedef unsigned long long ull;
typedef __int128 i128;
typedef double db;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
template <typename Type>
using vec=vector<Type>;
template <typename Type>
using grheap=priority_queue<Type>;
template <typename Type>
using lrheap=priority_queue<Type,vector<Type>,greater<Type> >;
#define fir first
#define sec second
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
#define chmax(a,b) a=max(a,b)
#define chmin(a,b) a=min(a,b)
#define rep(i,x,y) for(int i=(x);i<=(y);i++)
#define per(i,x,y) for(int i=(x);i>=(y);i--)
#define repl(i,x,y) for(int i=(x);i<(y);i++)
#define file(f) freopen(#f".in","r",stdin);freopen(#f".out","w",stdout);const int inf=0x3f3f3f3f;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int mod=/*1e9+7*/998244353;const int N=1e5+5;struct LCT{int son[N][2],fa[N];int rev[N];int val[N],sum[N];#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])#define get(x) (x==son[fa[x]][1])void pushup(int x){sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];}void pushdown(int x){if(rev[x]){if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;rev[x]^=1;}}void update(int x){if(!isroot(x))update(fa[x]);pushdown(x);}void rotate(int x){int y=fa[x],z=fa[y],k=get(x);if(!isroot(y))son[z][get(y)]=x;son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;son[x][k^1]=y,fa[y]=x,fa[x]=z;pushup(y);pushup(x);}void splay(int x){update(x);for(int f;f=fa[x],!isroot(x);rotate(x))if(!isroot(f))rotate(get(x)==get(f)?f:x);}int access(int x){int p;for(p=0;x;p=x,x=fa[x]){splay(x);son[x][1]=p;pushup(x);}return p;}void makeroot(int x){x=access(x);swap(son[x][0],son[x][1]);rev[x]^=1;}int find(int x){access(x);splay(x);pushdown(x);while(son[x][0])x=son[x][0],pushdown(x);splay(x);return x;}void link(int x,int y){makeroot(x);splay(x);fa[x]=y;}void split(int x,int y){makeroot(x);access(y);splay(y);}void cut(int x,int y){split(x,y);son[y][get(x)]=fa[x]=0;}void change(int x,int v){splay(x);val[x]=v;pushup(x);}
}lct;int n,m;
set<pii> st;int main(){ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;int v;rep(i,1,n)cin>>v,lct.change(i,v);int op,x,y;rep(i,1,m){cin>>op>>x>>y;if(op==0)lct.split(x,y),cout<<lct.sum[y]<<'\n';if(op==1)if(lct.find(x)!=lct.find(y))lct.link(x,y),st.insert({min(x,y),max(x,y)});if(op==2)if(st.count({min(x,y),max(x,y)}))lct.cut(x,y),st.erase({min(x,y),max(x,y)});if(op==3)lct.change(x,y);}return 0;
}