LCT+模板题

本篇文章根据洛谷的题解总结的https://www.cnblogs.com/flashhu/p/8324551.html

做到模板题涉及这个知识点就拿来总结了,想要明白明白这个知识点需要先学习一下树状链表和Splay平衡树的知识,Spaly之前总结过可以看之前的博文

动态树是一类维护森林连通性的问题,LCT(Link-Cut-Tree)就是一种动态树,应该还有很多别的类型的动态树,但是我觉得目前为止能够掌握lct的模板应该动态树的题目应该够用了,但是LCT也就只做了这一道模板题,其他的题目还没来得及做,理解的也可能没有那么深入

LCT的主要性质如下:

  1. 每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增。
    比如有一棵树,根节点为1(深度1),有两个儿子2,3(深度2),那么Splay有3种构成方式:
    {1−2},{3}
    {1−3},{2}
    {1},{2},{3}

  2. 1,2,3同放在一个Splay中(存在深度相同的点)

  3. 每个节点包含且仅包含于一个Splay中

  4. 边分为实边和虚边,实边包含在Splay中,而虚边总是由一棵Splay指向另一个节点(指向该Splay中中序遍历最靠前的点在原树中的父亲)。
    因为性质2,当某点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个Splay中的。
    那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的Splay的根节点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。

access(x)

LCT核心操作,也是最难理解的操作。其它所有的操作都是在此基础上完成的。因为我们不能总是保证两个点之间的路径是直接连通的(在一个Splay上)。access即定义为打通根节点到指定节点的实链,使得一条中序遍历以根开始、以指定点结束的Splay出现。

所以还是来几张图吧。

有一棵树,假设一开始实边和虚边是这样划分的(虚线为虚边)

那么所构成的LCT可能会长这样(绿框中为一个Splay,可能不会长这样,但只要满足中序遍历按深度递增就对结果无影响)

现在我们要access(N),把A−N的路径拉起来变成一条Splay。

因为该路径上其它链都要给这条链让路,也就是把每个点到该路径以外的实边变虚。

所以我们希望虚实边重新划分成这样。

然后怎么实现呢?

我们要一步步往上拉。

首先把splay(N),使之成为当前Splay中的根。

为了满足性质2,原来N-ON−O的重边要变轻。

因为按深度OO在NN的下面,在Splay中OO在NN的右子树中,所以直接单方面将NN的右儿子置为00(认父不认子)

然后就变成了这样——

我们接着把N所属Splay的虚边指向的I(在原树上是L的父亲)也转到它所属Splay的根,splay(I)。

原来在I下方的重边I-K要变轻(同样是将右儿子去掉)。

这时候I-L就可以变重了。因为LL肯定是在II下方的(刚才LL所属Splay指向了I),所以I的右儿子置为N,满足性质1。

然后就变成了这样——

I指向H,接着splay(H),H的右儿子置为I。

H指向A,接splay(A),A的右儿子置为H。

A−N的路径已经在一个Splay中了,大功告成!

代码其实很简单。。。。。。循环处理,只有四步——

  1. 转到根;
  2. 换儿子;
  3. 更新信息;
  4. 当前操作点切换为轻边所指的父亲,转1
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)

只是把根到某个节点的路径拉起来并不能满足我们的需要。更多时候,我们要获取指定两个节点之间的路径信息。

然而一定会出现路径不能满足按深度严格递增的要求的情况。根据性质1,这样的路径不能在一个Splay中。

makeroot定义为换根,让指定点成为原树的根。

这时候就利用到access(x)和Splay的翻转操作。

access(x)后x在Splay中一定是深度最大的点对吧。

splay(x)后,x在Splay中将没有右子树(性质1)。于是翻转整个Splay,使得所有点的深度都倒过来了,x没了左子树,反倒成了深度最小的点(根节点),达到了我们的目的。

代码

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);
}

关于pushdown和makeroot的一个相关的小问题详见下方update(关于pushdown的说明)

findroot(x)

找x所在原树的树根,主要用来判断两点之间的连通性(findroot(x)==findroot(y)表明x,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;
}

同样利用性质1,不停找左儿子,因为其深度一定比当前点深度小。

split(x,y)

神奇的makeroot已经出现,我们终于可以访问指定的一条在原树中的链啦!

split(x,y)定义为拉出x-y的路径成为一个Splay

代码

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

x成为了根,那么x到y的路径就可以用access(y)直接拉出来了,将y转到Splay根后,我们就可以直接通过访问y来获取该路径的有关信息

link(x,y)

连一条x−y的边(使x的父亲指向y,连一条轻边)

代码

inline bool link(int x,int y){
    makeroot(x);
    if(findroot(y)==x)return 0;//两点已经在同一子树中,再连边不合法
    f[x]=y;
    return 1;
}

如果题目保证连边合法,代码就可以更简单

inline void link(int x,int y){
    makeroot(x);
    f[x]=y;
}

cut(x,y)

将x-y的边断开。

如果题目保证断边合法,倒是很方便。

使x为根后,y的父亲一定指向x,深度相差一定是1。当access(y),splay(y)以后,x一定是y的左儿子,直接双向断开连接

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

那如果不一定存在该边呢?

充分利用好Splay和LCT的各种基本性质吧!

正确姿势——先判一下连通性(注意findroot(y)以后xx成了根),再看x,y是否有父子关系,还要看y是否有左儿子。

因为access(y)以后,假如y与x在同一Splay中而没有直接连边,那么这条路径上就一定会有其它点,在中序遍历序列中的位置会介于x与y之间。

那么可能y的父亲就不是x了。

也可能y的父亲还是x,那么其它的点就在y的左子树中

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;
}

如果维护了size,还可以换一种判断

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;
}

P3690 【模板】Link Cut Tree (动态树)

 

代码

最基本的LCT操作都在这里,也没有更多额外的复杂操作了,确实很模板。

#include<bits/stdc++.h>
#define R register int
#define I inline void
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
#define lc c[x][0]
#define rc c[x][1]
using namespace std;
const int SZ=1<<19,N=3e5+9;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
	G;while(*ip<'-')G;
	R x=*ip&15;G;
	while(*ip>'-'){x*=10;x+=*ip&15;G;}
	return x;
}
int f[N],c[N][2],v[N],s[N],st[N];
bool r[N];
inline bool nroot(R x){//判断节点是否为一个Splay的根(与普通Splay的区别1)
	return c[f[x]][0]==x||c[f[x]][1]==x;
}//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
I pushup(R x){//上传信息
	s[x]=s[lc]^s[rc]^v[x];
}
I pushr(R x){R t=lc;lc=rc;rc=t;r[x]^=1;}//翻转操作
I pushdown(R x){//判断并释放懒标记
	if(r[x]){
		if(lc)pushr(lc);
		if(rc)pushr(rc);
		r[x]=0;
	}
}
I rotate(R x){//一次旋转
	R 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);
}
I splay(R x){//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
	R y=x,z=0;
	st[++z]=y;//st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
	while(nroot(y))st[++z]=y=f[y];
	while(z)pushdown(st[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);
}
/*当然了,其实利用函数堆栈也很方便,代替上面的手工栈,就像这样
I pushall(R x){
	if(nroot(x))pushall(f[x]);
	pushdown(x);
}*/
I access(R x){//访问
	for(R y=0;x;x=f[y=x])
		splay(x),rc=y,pushup(x);
}
I makeroot(R x){//换根
	access(x);splay(x);
	pushr(x);
}
int findroot(R x){//找根(在真实的树中的)
	access(x);splay(x);
	while(lc)pushdown(x),x=lc;
	splay(x);
	return x;
}
I split(R x,R y){//提取路径
	makeroot(x);
	access(y);splay(y);
}
I link(R x,R y){//连边
	makeroot(x);
	if(findroot(y)!=x)f[x]=y;
}
I cut(R x,R y){//断边
	makeroot(x);
	if(findroot(y)==x&&f[y]==x&&!c[y][0]){
		f[y]=c[x][1]=0;
		pushup(x);
	}
}
int main()
{
	R n=in(),m=in();
	for(R i=1;i<=n;++i)v[i]=in();
	while(m--){
		R type=in(),x=in(),y=in();
		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信息的正确性
		}
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值