LCT
就是将各种数列的维护改到树上,如查询链上和,子树和,修改节点数值。
处理的基本思路是将树上问题重新转化到链上,而转化往往是基于给树上的节点按照某种顺序标号(DFS序or树链剖分),然后每次处理标号连续的一段或若干段。
DFS序
咕咕咕
树链剖分
咕咕咕
(以后复习到再写/cy)
LCT
\(link-cut-tree\)由 \(Tarjan\) 等人发明,可以处理更一般的动态树问题的,由势能分析得到均摊复杂度为\(O(n log n)\)。
动态维护的剖分
\(link-cut-tree\) 维护的是一片森林,能支持的操作包括:查询/修改某条链上的信息、砍掉一条边、在两个点之间连边(需要保证连接之后不可以出环)、换根。
\(lct\) 的实质是动态维护树的实链剖分/重链剖分——即将一棵树划分成若干条不相交的重链,每条重链使用一棵 \(Splay\)维护,没有重边的单点也视作一条重链。
每次通过将轻边打成重边的方式来打通某条特定的链。假如我们想得到点 \(u\) 到点 \(v\) 的链,方法是把它们打成重链,然后在重链中舍去两端的东西。
最终的目的就是让 \(u\) 和 \(v\) 同处在一棵 \(Splay\) 中,且这棵 \(Splay\) 只包含 \(u\) 到 \(v\) 这条路径上的点,这样我们就可以进行操作了。
实现
用这种方式表现轻边的连接方式:让每棵 Splay 的根的父亲指向这段重边的轻边父亲,而它却不是轻边父亲的左右儿子中的任何一个。
对于同一棵重链,我们约定靠上的点在左边,靠下的在右边。那么我们定义打通操作是将某个 Splay 的根和它的轻父亲打成同一棵 Splay,那么它父亲原来的右儿子就会退化成为轻儿子。我们成这个操作为 splice。
很轻松地我们可以将一个点一直 splice 到根,我们把通到根的操作称为 expose。将一个点 expose 到根之后,将它的右子树断掉,再将剩下的 Splay(只包含它到根的那条链)reverse 一下,就可以将这个节点变为根。
这样我们很简单地做完了换根 (reroot) 操作。提取链的操作更加容易:reroot(u); reroot(v); 那么此时的点 v 为根的 Splay 就为所求了。
还有两个操作 link 和 cut 没有处理,其实都是将两个节点 reroot 之后直接断/连接边就可以了。
性质
把在某个\(splay\)内部的边划分为实边,两个\(splay\)之间的划分为虚边。
每个\(splay\)维护一条原树中深度严格递增的路径,中序遍历\(splay\),得到的点序列深度严格递增。
每个点只属于一个\(splay\)。
一个父亲可能有多个儿子,在儿子中只有一个实儿子,让父亲指向的边变成虚边,由对应儿子所属的\(splay\)的根节点的父亲节点指向该点,这样的话父亲不能直接访问虚儿子。
虚边总是由一颗\(splay\),指向另一个\(splay\)中中序遍历最靠前的点的父亲。
(结合图感性理解一下

左边的树是重链剖分后的形态。
左树中的\(C\) -> \(A\),由于\(C\)在\(splay\)中的根节点为\(G\),所以\(G\) -> \(A\)。
即:在\(splay\)中找父节点,再用父节点与在原树中相连的点连接。
操作
access(n)
结果:把从根节点到\(n\)的路径变成实链,使其在 splay 的中序遍历中,以根节点开始,以指定点结束。
过程:
- 把\(n\)转到自己\(splay\)的根的位置。
- 断掉\(n\)的右儿子,实边 ->虚边。(因为右儿子深度更大,中序遍历会在\(n\)的后面,就不是以\(n\)结尾了。
- 把\(n\)所属的\(splay\)的虚边指向节点\(v\),转到属于它(虚边指向节点\(v\))的\(splay\)的根。
- 把\(v\)的右儿子断掉,把\(n\)连接成为\(v\)的右儿子(此时,\(n\)中仅含有左儿子,由于所在\(splay\)指向了\(v\),所以\(v\)的深度小于\(splay\)中的点。
- 重复执行。假设当前\(splay\)的根节点为\(x\) , \(fa(x)=y\) ,那么先\(splay(y)\)把y转到\(y\)所在\(splay\)的根节点,再断开\(y\)的右儿子,换成\(x\)。
trick:认父不认子。
code:
il void access(int x){
for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己
splay(x);//x->now father y->x
s[x][1]=y;//把右儿子变成last x
pushup(x);//儿子变了要上传信息
}
}
makeroot(x)
让\(x\)变成根。
很多时候,我们要处理的路径并不是从根到另一个节点的,而是没有祖孙关系的两个节点。
所以通过\(makeroot(x)\),让\(x\)变为原树的根。
过程:
-
access(x),让\(x\)和根在同一个\(splay\)中,同时让中序遍历最后一个节点是\(x\)(去掉右子树)
-
直接翻转。(\(splay\)可以很好维护区间翻转,具体方法是打标记。可参考文艺平衡树。
证明:
\(x\)原本没有右子树,翻转后\(x\)就没有了左子树,没有左子树的点,一定是\(splay\)中序遍历最小的点,既然我们保证\(splay\)中序遍历所经过的点深度严格单调递增,那么我们就让它成为了这个\(splay\)中深度最小的点,而这个\(splay\)是包含根节点的,根节点本来是深度最小的,这样\(x\)就变成了根。
说人话就是:本来在这个\(splay\)中,\(x\)是最后一个,翻转后就成了根,即第一个。
code:
il void pushr(int x){//翻转左右儿子
swap(s[x][0],s[x][1]);
tag[x]^=1;//翻转标记
}
il void pushdown(int x){//下传懒标记
if(tag[x]){
if(s[x][0]) pushr(s[x][0]);
if(s[x][1]) pushr(s[x][1]);
tag[x]=0;
}
}
il void makeroot(int x){
access(x);//打通x
splay(x);//把x转到根
pushr(x);//翻转后打上标记
}
findroot(x)
主要用于判断连通性,找到\(x\)所在原树的根,如果 findroot(x)==findroot(y)/mekeroot(x),if(findroot(y)==x)说明两个点在同一个树里。
il int findroot(int x){//常数大 尽量减少使用
access(x);splay(x);//把x转到根
while(s[x][0]){//找左儿子->不断靠近根,深度逐渐减小
pushdown(x);
x=s[x][0];
}//找到根节点之后停止,把根节点再转回去
splay(x);
return x;
}
split(x,y)
就是拿出一棵 \(Splay\),维护的是 \(x\) 到 \(y\) 的路径。
il void spilt(int x,int y){
makeroot(x);//让x->根
access(y);//打通y->x的路
splay(y);//把y转到根,视情况而决定加不加
}
link(x,y)
连接\(x\), \(y\),先将\(x\)转到根,再把\(x\)的父亲设为\(y\),建立了一条虚边。
il void link(int x,int y){
makeroot(x);//把x转到根
if(findroot(y)!=x) f[x]=y;//判断是否合法,连接虚边不能在同一棵splay中
}
cut(x,y)
\(Cut\) 有两种情况,保证合法和不一定保证合法。
如果保证合法,直接 Split(x, y),这时候 y 是根,x 一定是它的儿子,双向断开即可。就像这样:
inline void Cut(int x, int p) {
makeRoot(x), Access(p), Splay(p), ls = f[x] = 0;
}
如果是不保证合法,我们需要判断一下是否有,可以选择使用 \(map\) 存一下,但是这里有一个利用性质的方法:
想要删边,必须要满足如下三个条件:
- \(x\) ,\(y\) 连通。
- \(x\) , \(y\) 的路径上没有其他的链。
- \(x\) 没有右儿子。
总之就是\(x\) ,\(y\) 之间有边。
il void cut(int x,int y){
makeroot(x);//让x做根
if(findroot(y)==x&&f[y]==x&&!s[y][0]){//如果在同一个splay中并且x,y的路径上没有其他的链并且x没有右儿子
f[y]=s[x][1]=0;
pushup(x);
}
}
IsRoot
判断是否为根。
il bool IsRoot(int x){//s[f[]][0/1]父亲的儿子信息里面只有实儿子
return s[f[x]][0]==x||s[f[x]][1]==x;
}
splay相关
pushdown
要从上往下传递标记,所以要开一个栈来储存从当前点到根的整条路径,然后先\(pushdown\),再用普通\(splay\)的方式上传。
il void splay(int x){
int y=x,z=0;
st[++z]=y;//栈来储存经过的点
while(IsRoot(y)) st[++z]=y=f[y];//将标记沿着路径下传
while(z) {pushdown(st[z--]);}
while(IsRoot(x)){//正常splay
y=f[x];z=f[y];
if(IsRoot(y))
rotate((s[y][0]==x)^(s[z][0]==y)?x:y);
rotate(x);
}
pushup(x);
}
其他具体情况具体分析。
例题
const int N=1e5+10;
const int M=3e5+10;
int n,m,op,x,y,w[N];
namespace LCT{
#define ls(k) s[k][0]
#define rs(k) s[k][1]
int s[N][2];
int f[N],sum[N];//路径权值和
int tag[N];//点权,翻转标记
int laz[N],siz[N];//辅助树上子树大小
int st[N];//开栈
il void pushup(int k){siz[k]=siz[ls(k)]^siz[rs(k)]^w[k];}
il void pushr(int x){//翻转左右儿子
swap(s[x][0],s[x][1]);
tag[x]^=1;//翻转标记
}
il void pushdown(int x){//下传懒标记
if(tag[x]){
if(s[x][0]) pushr(s[x][0]);
if(s[x][1]) pushr(s[x][1]);
tag[x]=0;
}
}
il bool IsRoot(int x){//s[f[]][0/1]父亲的儿子信息里面只有实儿子
return s[f[x]][0]==x||s[f[x]][1]==x;
}
il void rotate(int x){//旋转
int y=f[x],z=f[y];//y是x的父亲,z是x的爷爷
int k=s[y][1]==x;//k代表x是y的左儿子还是右儿子
if(IsRoot(y)) s[z][s[z][1]==y]=x;
s[y][k]=s[x][k^1],f[s[x][k^1]] = y;
s[x][k^1]=y,f[y]=x,f[x]=z;
pushup(x);pushup(y);
}
il void splay(int x){
int y=x,z=0;
st[++z]=y;//栈来储存经过的点
while(IsRoot(y)) st[++z]=y=f[y];//将标记沿着路径下传
while(z) {pushdown(st[z--]);}
while(IsRoot(x)){//正常splay
y=f[x];z=f[y];
if(IsRoot(y))
rotate((s[y][0]==x)^(s[z][0]==y)?x:y);
rotate(x);
}
pushup(x);
}
il void access(int x){
for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己
splay(x);//x->now father y->x
rs(x)=y;//把右儿子变成last x
pushup(x);//儿子变了要上传信息
}
}
/*
il void access(int x){
for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己
splay(x);//x->now father y->x
s[x][1]=y;//把右儿子变成last x
pushup(x);//儿子变了要上传信息
}
}
*/
il void makeroot(int x){
access(x);//打通x
splay(x);//把x转到根
pushr(x);//翻转后打上标记
}
il int findroot(int x){//常数大 尽量减少使用
access(x);splay(x);//把x转到根
while(ls(x)){//找左儿子->不断靠近根,深度逐渐减小
pushdown(x);
x=ls(x);
}//找到根节点之后停止,把根节点再转回去
splay(x);
return x;
}
il void split(int x,int y){
makeroot(x);//让x->根
access(y);//打通y->x的路
splay(y);//把y转到根,视情况而决定加不加
}
il void cut(int x,int y){
makeroot(x);//让x做根
if(findroot(y)==x&&f[y]==x&&!s[y][0]){//如果在同一个splay中并且x,y的路径上没有其他的链并且x没有右儿子
f[y]=s[x][1]=0;
pushup(x);
}
}
il void link(int x,int y){
makeroot(x);//把x转到根
if(findroot(y)!=x) f[x]=y;//判断是否合法,连接虚边不能在同一棵splay中
}
}
signed main(){
n=read();m=read();
rep(i,1,n)
w[i]=read();
rep(i,1,m){
op=read();x=read();y=read();
if(op==0){
LCT::split(x,y);
cout<<LCT::siz[y]<<'\n';
}
else if(op==1){
LCT::link(x,y);
}
else if(op==2){
LCT::cut(x,y);
}
else if(op==3){
LCT::splay(x);w[x]=y;//把x先转到根上去
}
}
return 0;
}
const int N=1e5+10;
const int M=3e5+10;
int n,m,x,y;
string op;
namespace LCT{
#define ls(k) s[k][0]
#define rs(k) s[k][1]
int s[N][2];
int f[N],sum[N];//路径权值和
int tag[N];//点权,翻转标记
int laz[N],siz[N];//辅助树上子树大小
int st[N];//开栈
il void pushup(int k){siz[k]=siz[ls(k)]+siz[rs(k)]+1;}//Wrong
il void pushr(int x){//翻转左右儿子
swap(s[x][0],s[x][1]);
tag[x]^=1;//翻转标记
}
il void pushdown(int x){//下传懒标记
if(tag[x]){
if(s[x][0]) pushr(s[x][0]);
if(s[x][1]) pushr(s[x][1]);
tag[x]=0;
}
}
il bool IsRoot(int x){//s[f[]][0/1]父亲的儿子信息里面只有实儿子
return s[f[x]][0]==x||s[f[x]][1]==x;
}
il void rotate(int x){//旋转
int y=f[x],z=f[y];//y是x的父亲,z是x的爷爷
int k=s[y][1]==x;//k代表x是y的左儿子还是右儿子
if(IsRoot(y)) s[z][s[z][1]==y]=x;//Wrong
s[y][k]=s[x][k^1],f[s[x][k^1]] = y;
s[x][k^1]=y,f[y]=x,f[x]=z;
pushup(x);pushup(y);
}
il void splay(int x){
int y=x,z=0;
st[++z]=y;//栈来储存经过的点
while(IsRoot(y)) st[++z]=y=f[y];//将标记沿着路径下传
while(z) {pushdown(st[z--]);}
while(IsRoot(x)){//正常splay
y=f[x];z=f[y];
if(IsRoot(y))
rotate((s[y][0]==x)^(s[z][0]==y)?x:y);
rotate(x);
}
pushup(x);
}
il void access(int x){
for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己
splay(x);//x->now father y->x
rs(x)=y;//把右儿子变成last x
pushup(x);//儿子变了要上传信息
}
}
il void makeroot(int x){
access(x);//打通x
splay(x);//把x转到根
pushr(x);//翻转后打上标记
}
il int findroot(int x){//常数大 尽量减少使用
access(x);splay(x);//把x转到根
while(ls(x)){//找左儿子->不断靠近根,深度逐渐减小
pushdown(x);
x=ls(x);
}//找到根节点之后停止,把根节点再转回去
splay(x);
return x;
}
il void split(int x,int y){
makeroot(x);//让x->根
access(y);//打通y->x的路
splay(y);//把y转到根,视情况而决定加不加
}
il void cut(int x,int y){
makeroot(x);//让x做根
if(findroot(y)==x&&f[y]==x&&!s[y][0]){//如果在同一个splay中并且x,y的路径上没有其他的链并且x没有右儿子
f[y]=s[x][1]=0;
pushup(x);
}
}
il void link(int x,int y){
makeroot(x);//把x转到根
if(findroot(y)!=x) f[x]=y;//判断是否合法,连接虚边不能在同一棵splay中
}
}
signed main(){
n=read();m=read();
rep(i,1,m){
cin>>op;x=read();y=read();
if(op[0]=='Q'){
LCT::makeroot(x);
if(LCT::findroot(y)==x) cout<<"Yes"<<'\n';
else cout<<"No"<<'\n';
}
else if(op[0]=='C'){
LCT::link(x,y);
}
else if(op[0]=='D'){
LCT::cut(x,y);
}
}
return 0;
}

浙公网安备 33010602011771号