Link-Cut Tree(知识总结+板子整理)

思路来源

https://www.luogu.com.cn/blog/flashblog/solution-p3690

https://www.cnblogs.com/19992147orz/p/8206693.html

https://www.cnblogs.com/candy99/p/6271344.html

前置知识

splay:真正理解rotate、会区间翻转

树链剖分:轻重链的概念

心得

杭电多校LCT的题,被120队过,不学新知识不行了,哎……

强推第一篇洛谷题解,有图可供食用,感觉还是挺好的……

然而,蒟蒻还是学了4个多小时……

access(x)

打通x到当前根的路径,

打通之后,x和根在同一Splay里,且x是深度最大的点,

即把x到根的这几棵Splay直接的虚边变为实边,原先对应的实边改为虚边

 

具体操作:

先把x旋到根,Splay(x),

并把x的右子树置空,c[x][1]=0,强行切断x和比x深度更大的点的连接

由于x少了一个右子树,更新x这个根节点维护的信息,pushup(x)

 

x从虚边跳到深度更浅的一棵Splay,不妨令y=f(x),则跳到了y这个点

重复这个过程,把y旋到根,Splay(y),

把y的右子树置空,变原实边为虚边,c[y][1]=0,表示切断y与y原来的右子树之间的连接,

将右子树置为上一个操作的Splay的根,变原虚边为实边,c[y][1]=x,表示连接y这棵Splay与x这棵Splay

事实上,由于是赋值语句,可以忽略置空这个操作

由于y换了一个右子树,更新y这个根节点维护的信息,pushup(y)

 

y跳到深度更浅的一棵Splay,重复上述过程,直至跳到原树的根

inline void access(int x){
	for(int y=0;x;y=x,x=f[x])
		splay(x),c[x][1]=y,pushup(x);//儿子变了,需要及时上传信息
}

makeroot(x)

换根,把x定为原树的根,

实际上,先打通x与原来的根rt的Splay,access(x)

然后,将这棵Splay打一个区间翻转的标记,pushr(x),

r[x]懒惰标记:下放时,x的儿子ls、rs的左右子树应当被交换,且其标记应翻转

 

由于原来的Splay是按深度增序维护的,

x作为深度最大的点,被Splay到根之后,x没有右子树,

打一个区间翻转的标记之后,左右子树互换,x最终没有左子树,

这表明x是这棵Splay里深度最小的点,起到了"本末倒置"的作用

相当于换了一个节点,将整棵树拽出来,有种提灯笼的感觉

inline void pushr(int x){//Splay区间翻转操作
    swap(c[x][0],c[x][1]);
    r[x]^=1;//r为区间翻转懒标记数组
}
inline void makeroot(int x){
    access(x);splay(x);
    pushr(x);
}

findroot(x)

找到x所在的原树的树根,

主要用于判断x和y两个点是否在一棵树上,即连通性

先打通x到根rt,access(x)

再将x旋到根,splay(x),保证复杂度

不断向左子树找,并沿途下放x的标记,保证x的左子树是翻转后的,

直至找到最左的点,这是深度最小的点,记为y,

splay(y),并返回y的值

inline int findroot(R x){
    access(x); splay(x);
    while(c[x][0])pushdown(x),x=c[x][0];
//如要获得正确的原树树根,一定pushdown!详见下方update(关于findroot中pushdown的说明)
    splay(x);//保证复杂度
    return x;
}

split(x,y)

将(x,y)在树上的路径,放到一个Splay里当实链

树上问题,常询问(x,y)点对之间的权值和/异或和之类,需要顾及两条链

先把其中一个点,不妨为x,换为根,makeroot(x)

再把y到根的Splay打通,access(y),y是深度最大的点

把y旋到根,splay(y),这样x及其余点都在y左子树里

inline void split(int x,int y){
    makeroot(x);
    access(y);splay(y);
}

link(x,y)

连一条x-y的边,注意判断树上x-y间是否已经有边,

先把x指定为根,makeroot(x),

再判一下x和y的连通性即可,findroot(y)==x,若连通再连边就成环了

否则说明x和y在两棵树上,令x这棵树引一条虚边到y即可,f[x]=y

inline bool link(int x,int y){
    makeroot(x);
    if(findroot(y)==x)return 0;//两点已经在同一子树中,再连边不合法,如果保证连边一定合法,则可省略本行
    f[x]=y;
    return 1;
}

cut(x,y)

断开x-y在原树上的边,动态删边

①保证删边合法

先扒出(x,y)在树上的路径,split(x,y),注意split里的操作

先makeroot(x),此时x是原树的根,

再access(y),打通y到x的路径,Splay里只有y到x的路径,

再Splay(y),此时y在Splay里是根,

则x由于与y相连,深度比y小1,一定是y的直连左子树,

直接令f[x]=0,表示x的父亲不存在;

c[y][0]=0,表示y的左子树不存在,并更新y的信息,pushup(y)

inline void cut(int x,int y){
    split(x,y);
    f[x]=c[y][0]=0;
    pushup(y);//少了个儿子,也要上传一下
}

②删边不一定合法,即不能保证原树上x-y之间有边

 

先将x钦定为根,makeroot(x)

(1)判断一下y和x的连通性,若findroot(y)!=x,表示y和x在原树上不连通,无边,否则转(2)

(2)注意到(1)中执行了findroot(y),其中包含access(y)和splay(y)和splay(x),

现在x和y的路径被打通,x是Splay的根,Splay中只有x到y的路径,

若y在Splay中的直连父亲不是y,f[y]!=x,说明y和x之间还有别的点,否则转(3)

(3)此时f[y]==x已经成立,但可能y是x的右子树,y还有左子树,导致中序遍历时xy之间还有点,

所以如果ch[y][0]!=0,说明y和x之间还有别的点,否则说明这是合法的情况

inline bool cut(int x,int y){
	makeroot(x);
	if(findroot(y)!=x||f[y]!=x||c[y][0])return 0;
	f[y]=c[x][1]=0;//x在findroot(y)后被转到了根
	pushup(x);
	return 1;
}

合法的情况:x、y的Splay里只有x、y两个点,x为根,y为x的右子树

所以,如果维护了子树的size信息,先判连通,判完连通之后,

x为原树根且为Splay的根,如果sz[x]==2则合法,否则不合法

inline bool cut(int x,int y){
    makeroot(x);
    if(findroot(y)!=x||sz[x]>2)return 0;
    f[y]=c[x][1]=0;
    pushup(x);
    return 1;
}

nroot(x)

not root的缩写,用于判断x是不是Splay的根,返回1代表不是Splay的根

f[x]的含义:

如果x不是Splay的根,f[x]=z是实边,代表x在Splay里的父亲z

如果x是Splay的根,记x所在的Splay里深度最小的点为y,f[x]=z是虚边,代表y在原树中的父亲z

ch[x][0/1]的含义:

仅用于同一棵Splay的点的左右孩子,可以认为是实边的反向边

inline bool nroot(R x){//判断节点是否为一个Splay的根(与普通Splay的区别1)
	return c[f[x]][0]==x||c[f[x]][1]==x;
}//原理很简单,如果连的是轻边,他的父亲的儿子里没有它

个人体会

LCT:Link-Cut Tree,动态树

像树剖分轻重链一样,动态树维护的是一个森林,森林上有虚链和实链

每个点与Splay里的点号一致,共n个点,

每个点往下只有一条链是实链,剩下全是虚链

①每个点恰好出现在一个Splay里

②一个Splay按深度关键字维护一条实链,即中序遍历这棵Splay,可以按深度增序访问这条实链

虚链的边:实际上指向的是这棵Splay里编号最小的点在原树中的父亲,只能从子找到父亲,认父不认子

实链的边:一棵Splay内部的边,既能通过f[]找父亲,也能通过ch[][0/1]找儿子

③由于Splay的根还有父亲,即虚链边对应的父亲,

所以Splay中rotate的时候,考虑x y z的时候,x的父亲是y,y的父亲是z,

z可以为虚链边的父亲,但这种情况下仍需特判,因为此时z不参与旋转,

这样,设换根之前根是x,虚链点是z,f[x]=z,则换根后设根是y,必有f[y]=z

板子整理

以洛谷P3690 动态树模板为例

#include<bits/stdc++.h>
#define lc c[x][0]
#define rc c[x][1]
using namespace std;
const int N=3e5+9;
//f:父亲 c:儿子 v:单点值 s:子树值 stk:用于从上到下释放标记的栈 r:区间翻转标记
int n,m,f[N],c[N][2],v[N],s[N],stk[N];
bool r[N];
bool nroot(int x){//判断节点是否为一个Splay的根(与普通Splay的区别1)
	return c[f[x]][0]==x||c[f[x]][1]==x;
}//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
void pushup(int x){//上传信息
	s[x]=s[lc]^s[rc]^v[x];
}
void pushr(int x){int t=lc;lc=rc;rc=t;r[x]^=1;}//翻转操作
void pushdown(int x){//判断并释放懒标记
	if(r[x]){
		if(lc)pushr(lc);
		if(rc)pushr(rc);
		r[x]=0;
	}
}
void rotate(int x){//一次旋转
	int y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
	if(nroot(y))c[z][c[z][1]==y]=x;c[x][!k]=y;c[y][k]=w;//额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
	if(w)f[w]=y;f[y]=x;f[x]=z;
	pushup(y);
}
void splay(int x){//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
	int y=x,z=0;
	stk[++z]=y;//st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
	while(nroot(y))stk[++z]=y=f[y];
	while(z)pushdown(stk[z--]);
	while(nroot(x)){
		y=f[x];z=f[y];
		if(nroot(y))
			rotate((c[y][0]==x)^(c[z][0]==y)?x:y);
		rotate(x);
	}
	pushup(x);
}
void access(int x){//访问
	for(int y=0;x;x=f[y=x]){
        splay(x),rc=y,pushup(x);
	}
}
void makeroot(int x){//换根
	access(x);splay(x);
	pushr(x);
}
int findroot(int x){//找根(在真实的树中的)
	access(x);splay(x);
	while(lc)pushdown(x),x=lc;
	splay(x);
	return x;
}
void split(int x,int y){//提取路径
	makeroot(x);
	access(y);splay(y);
}
void link(int x,int y){//连边
	makeroot(x);
	if(findroot(y)!=x)f[x]=y;
}
void cut(int x,int y){//断边
	makeroot(x);
	if(findroot(y)==x&&f[y]==x&&!c[y][0]){
		f[y]=c[x][1]=0;
		pushup(x);
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
        scanf("%d",&v[i]);
	}
	while(m--){
	    int type,x,y;
	    scanf("%d%d%d",&type,&x,&y);
		switch(type){
            case 0:split(x,y);printf("%d\n",s[y]);break;
            case 1:link(x,y);break;
            case 2:cut(x,y);break;
            case 3:splay(x);v[x]=y;//先把x转上去再改,不然会影响Splay信息的正确性,子树都是对的,x及以上的点会先pushup(x)
        }
	}
	return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值