动态树LCT

LCT详解

引入

思考对于一棵树,如果我们需要维护这棵树上的某些信息,如一条链上的最大值,权值和等,我们可以使用树链剖分很好的维护。但是我们如果要求解一个树上的异或和,我们倘使使用线段树维护,则树链剖分的时间复杂度高达 n l o g 2 n nlog^{2}n nlog2n,这个效率可谓及其的低下,稍不注意就可能超时。而且如果我们的树是动态的(比如说我们需要在两点之间加边或删边),我们的树链剖分就没有任何办法了,在这个时候,我们就要使出我们的动态树

LCT

动态树Link-Cut Tree,简称 L C T LCT LCT。由他的名字我们可以知道,这个数据结构可以实现连接(Link)切断(Cut),并且几乎可以完成所有树链剖分的操作。需要注意的一点是, L C T LCT LCT并不叫动态树,他只是动态树的一种。还有其他的动态树。

例题

P3690 【模板】动态树(LCT)
下面,我将为你逐步分解这道例题

结构体定义

我们先要定义好我们的结构体,先把 L C T LCT LCT的原理放一放。现在你只需要知道 L C T LCT LCT是用 S p l a y Splay Splay实现的即可,所以LCT的结构体和 S p l a y Splay Splay几乎一致。
Code:

struct lit{
	ll val,id,sum;//sum维护异或和
	ll rev;//rev是区间翻转的标记,下面会解释
	lit *l,*r,*p;
	lit(ll i,ll v){
		id=i;sum=val=v;rev=0;
		rev=0;
		l=r=p=nullptr;
	}
};

LCT的基本原理

LCT中有两个基本的概念:偏好路径偏好儿子
我们先明确一下我们的 S p l a y Splay Splay维护的是什么顺序???我们是以深度维护的整个 S p l a y Splay Splay
思考对于一个树上的节点,他可能有很多个儿子,但他在 S p l a y Splay Splay中只可能有一个右儿子,所以我们要引入偏好儿子来维护他。
显然我们的偏好儿子是可以认他的父亲的,他的父亲也要认偏好儿子,那其他儿子呢?对于其他儿子,我们不可能父认子,那就只能子认父了,这也是一个重要的思想,就是只记录你要求的路径上的值,不管其他的路径。

splay(伸展)

由于我们用到了 S p l a y Splay Splay,那肯定少不了旋转与伸展。
我们需要将节点 u u u伸展到根。在 S p l a y Splay Splay模板中,我们是要将 u u u节点伸展到整棵树的根,但是在这里,我们只需要将其伸展到其所在偏好路径的根即可。注意一点,我们由于要区间翻转(理由下面说),我们在进行伸展之前,要下传标记。
Code:

inline bool isRight(lit *u){//判断u是不是右儿子
		if(!u)return 0;
		return u->p&&u->p->r==u;
	}
	inline bool isRoot(lit *x){//判断u是否为根节点
		if(!x)return 0;
		return !x->p||(x!=x->p->l&&x!=x->p->r);
		//没有父亲节点代表他为整个根,父亲节点不认这个儿子说明u与其父亲间为轻边
	}
	inline ll getsum(lit *u){return u?u->sum:0;}
	inline void pushup(lit *u){
		if(!u)return ;
		u->sum=u->val;
		u->sum^=getsum(u->l)^getsum(u->r);
	}
	inline void mark_co(lit *u){
		if(!u)return ;
		u->rev^=1;
	}
	inline void pushdown(lit *u){
		if(!u||!u->rev)return;
		swap(u->l,u->r);
		mark_co(u->l);mark_co(u->r);
		u->rev=0;
	}
	inline void rotate(lit *u){//splay的旋转
		if(!u||!u->p)return ;//没有就不旋转
		lit *p=u->p;
		lit *g=p->p;
		pushdown(p);pushdown(u);//记得下先下传p,再u
		if(p->l==u){//u为左儿子
			p->l=u->r;//由上往下更新
			if(u->r)u->r->p=p;
			u->r=p;
		}
		else{
			p->r=u->l;
			if(u->l)u->l->p=p;
			u->l=p;
		}
		p->p=u;
		u->p=g;
		if(g){//最后更新g
			if(g->l==p)g->l=u;
			else if(g->r==p)g->r=u;
		}
		pushup(u);pushup(p);
	}
	inline void splay(lit *x){//伸展
		if(!x)return ;
		vector<lit*> stack;
		lit *u = x;
		while (u) {//首先将从u到根的节点都下传
			stack.push_back(u);
			u = u->p;
		}
		while (!stack.empty()) {
			pushdown(stack.back());
			stack.pop_back();
		}
		while (!isRoot(x)) {//一直旋转到根
			lit *p= x->p;
			if (!isRoot(p)) {//p不为根则双旋
				if (isRight(x)==isRight(p))rotate(p);//同侧先旋p
				else rotate(x);//异侧先旋为同侧
			}
			rotate(x);
		}
		pushup(x);//记得更新
	}

accesss(连接)

a c c e s s access access L C T LCT LCT的根本,后续我们地操作基本都是以他为基础。 a c c e s s access access的作用是打通一条从 u u u树根的偏好路径,注意是树根。这个操作虽然很重要,但是没什么好说的。上文我们已经说了 L C T LCT LCT 子认父的性质,那我们只需要从 u u u一直往上跳即可。由于偏好路径父也要认子,所以我们要注意改变父节点的儿子。
Code:

inline void access(lit *u){//打通u到根的偏好路径
		if(!u)return ;
		lit *y=nullptr;//y记录儿子
		while(u){
			splay(u);//先将u伸展到根
			u->r=y;//由于y的深度大于u,则y只可当u的右儿子
			if(y)y->p=u;//连边
			pushup(u);
			y=u;
			u=u->p;//u向上跳
		}
		
	}

makeroot(设置为根节点)

讲完 a c c e s s access access操作,下面来讲几个我们会用到的操作。
m a k e r o o t makeroot makeroot这个操作是将 u u u节点设为整棵树的根节点。这个操作怎么实现呢?我们画个图方便理解一下:
在这里插入图片描述
我们把 x x x变为根后图如下:
在这里插入图片描述
我们发现对于其他部分,他们没有任何关系。对于rt到x的路径,他们的相对深度反了过来,那我们直接将这个区间翻转即可,这就是我们翻转标记的作用。
到这里代码就很显然了。
Code:

inline void makeroot(lit *u){//将u变为整棵树的根
		if(!u)return ;
		access(u);//先将u到根的偏好路径打通,则此时u必定在当前偏好路径的最后一个
//变为根后,原来的根深度最低,u最高,其他的顺序相反,所以直接翻转即可
		splay(u);
		u->rev^=1;
	}

findroot(查找树的根)

f i n d r o o t findroot findroot是用来查找 u u u的根的。想要实现这个操作,首先我们肯定先打通偏好路径在将 u u u伸展到根。由于根节点的深度是最低的,所以我们只需要一直往左边跑即可。记得下传标记!

inline lit *findroot(lit *x){//查找x所在根
		if(!x)return nullptr;
		access(x);//先打通偏好路径
		splay(x);
		while(x->l){pushdown(x);x=x->l;}//根节点深度最低,在最左边
		splay(x);
		return x;
	}

split(分裂某段路径)

这个操作是为了路径查询用的。他可以将路径 ( x , y ) (x,y) (x,y)的值保存在 y y y上。
我们直接先将 x x x设为根,在打通 x x x y y y的路径,最后将 y y y伸展到根即可。
Code:

inline void split(lit *x,lit *y){//分裂出x-y的路径,y为其的根
		makeroot(x);//先将x变为根
		access(y);//在打通y到x的偏好路径,此时y-x为一个splay
		splay(y);
	}

link(连边)

讲了这么久,终于可以开始操作了。 l i n k link link L C T LCT LCT的基本操作。
我们在进行操作之前先要开一个数组,记录对应点在 S p l a y Splay Splay中所对应节点。
我们先将 x x x设为根,为了防止边建重了,我们先要判断一下 y y y的根是否为 x x x,如果不是则建边。但是,要注意,在这了我们建的是轻边,及非偏好路径。所以我们只需要子认父即可。
由于x为根,所以只需要 x x x y y y即可。
Code:

inline void build(ll n){//建树
		node.resize(n+1,nullptr);
		for(ll i=1;i<=n;i++)node[i]=new lit(i,w[i]);
	}
	inline void link(ll y,ll x){//连接x-y,反向连接玄学加速
		if(x<0||x>=(ll)node.size()||!node[x]||y<0||y>=(ll)node.size()||!node[y])return;
		//排除x,y不存在的情况
		makeroot(node[x]);//先将x变为根
		if(findroot(node[y])!=node[x])node[x]->p=node[y];
//倘使y的根不为x,则建立轻边
	}

cut(删边)

对于删边操作,我们肯定也先要将 x x x变为根,在打通 y y y x x x的路径后,将 y y y旋到根,若 x , y x,y x,y之间有边,则此时 x x x必定为 y y y的左儿子,因为 x x x的深度小于 y y y。此时我们只需要将 x − > p x->p x>p y − > l y->l y>l变为空指针即可。注意,此时我们一定要 p u s h u p ( y ) pushup(y) pushup(y),因为 y y y的子树改变了。

inline void cut(ll x,ll y){//删除边x-y
		if(x<0||x>=(ll)node.size()||!node[x]||y<0||y>=(ll)node.size()||!node[y])return;
		makeroot(node[x]);//也是先将x设为根
		access(node[y]);splay(node[y]);
		if(node[y]->l==node[x]){
//若此时y的根为x,且y为x的子节点
			node[x]->p=nullptr;node[y]->l=nullptr;
			pushup(node[y]);
		}
	}

change(单点修改)

对于一个点,我们只需要将他伸展到根,然后直接修改即可

void change(ll x,ll v){
		if(x<=0||x>(ll)node.size()||!node[x])return ;
		splay(node[x]);node[x]->val=v;pushup(node[x]);
	}

query(查询)

只需要将查询的边用 s p l i t split split函数分出来,再直接查询答案即可。

ll query(ll x,ll y){
		if(x<=0||x>(ll)node.size()||!node[x]||y<=0||y>(ll)node.size()||!node[y])return 0;
		split(node[x],node[y]);
		return getsum(node[y]);
	}

总结

L C T LCT LCT的基础学习就到这里,真正难的不是数据结构,而是对数据结构的使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值