LCT学习笔记

实现

从例题开始:

P3690 【模板】动态树(LCT)

对于一棵静态的树,常见方法是树剖然后走链,但是在动态的情况下常见的重链或长链就会很慢,因为修改连边情况后就不满足性质了

引入一个新的方法:实链剖分,对于一个节点,任选一个儿子,连边为实边,其余为虚边,注意这里的实边是可以变化的

但是显然这样剖分的形式就不固定了,所以之前线段树维护的方法就不成立了,但是可以用 splay 解决

此时树变成了若干条实边和虚边,连接在一起的实边成为实链

对每条实链进行维护,使得 splay 的中序遍历为原树上深度从小到大排序的结果

对于虚边,作用是把这些 splay 连起来,方式如下:

  1. 找到该 splay 深度最小的节点,记为 k,若为根则不管

  2. 找到它的 fa,由它向当前 splay 的根连边

因为一个节点会有多个儿子,但是它只会存一个,就是认父不认子

区别于普通 splay,因为实链之间不能相互影响,操作时还需要判断是否为当前 splay 的根,如果是,返回-1,rotate 和 splay 函数区别不大

access:就是把点 x 到根路径上的点全部变成实边

首先将 x 转到当前 splay 的根,然后将 x 与 fa 之间的边变成实边,就是放到右儿子,然后处理 fa 所在子树,重复此操作直到整棵树的根

同时因为将 x 到根打包成一个 splay,所以 x 原来的实儿子变成虚儿子

void access(int x)
{
	int t=0;
	while(x)
	{
		splay(x);
		tr[x].rs=t;
		pushup(x);
		t=x,x=tr[x].f;
	}
}

makeroot:将 x 节点设为所在联通快的根

首先打通 x 到根的路径,此时 x 一定在这个子树的中序遍历的最后一位

如果我们将它设为根,那么它就是深度最小的点,但是他的 fa 作为深度第二大的点,理应在倒数第二位,翻转后会在第二位,这部分可以打懒标记实现

所以最后就是打通 x 到根的路径,然后把 x 转到根,给 x 打标记

同时将 x 转到根时,路径上的标记要全部下放

void makeroot(int x)
{
	access(x);
	splay(x);
	swap(tr[x].ls,tr[x].rs);
	if(tr[x].ls) tr[tr[x].ls].lazy^=1;
	if(tr[x].rs) tr[tr[x].rs].lazy^=1;
}

split:就是把 x 到 y 的路径变成实链,然后分成新的 splay,操作上就是先把 x 换到整棵树的根,然后打通 y 到根的路径,最后将 y splay 到子树的根

findroot:就是找到 x 所在点的实链的根,可以先将 x 转到根,此时因为根深度最小,暴力往左边找,注意下放标记,最后要转回去

link:连边,将 x 换到根,然后判断 y 和 x 是否在同一个联通快,不在就连一条虚边

cut:断边,先判断是否在一个联通快内,然后将路径独立出来,此时 y 若与 x 相连,则 y 在 x 的父亲且中序遍历上相邻,所以 x 不能有右子树

最后整道题就是这样的:

点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m;
struct node{
	int f,ls,rs,val,sum,lazy;
}tr[N];
void pushup(int p)
{
	tr[p].sum=tr[tr[p].ls].sum^tr[tr[p].rs].sum^tr[p].val;
}
void pushdown(int x)
{
	if(tr[x].lazy)
	{
		swap(tr[x].ls,tr[x].rs);
		if(tr[x].ls) tr[tr[x].ls].lazy^=1;
		if(tr[x].rs) tr[tr[x].rs].lazy^=1;
		tr[x].lazy=0;
	}
}
int get(int x)
{
	if(tr[tr[x].f].ls==x) return 0;
	if(tr[tr[x].f].rs==x) return 1;
	return -1;
}
void change(int x,int f,int k)
{
	tr[x].f=f;
	if(k==1) tr[f].rs=x;
	if(k==0) tr[f].ls=x;
}
void rotate(int x)
{
	int y=tr[x].f,z=tr[y].f;
	int k=get(x),k1=get(y);
	int u=0;
	if(k==1) u=tr[x].ls;
	if(!k) u=tr[x].rs;
	change(u,y,k),change(y,x,k^1),change(x,z,k1);
	pushup(y),pushup(x);
}
void pushall(int x)
{
	if(get(x)!=-1) pushall(tr[x].f);
	pushdown(x);
}
void splay(int x)
{
	pushall(x);
	while(get(x)!=-1)
	{
		int y=tr[x].f;
		if(get(y)!=-1)
		{
			(get(x)^get(y))?rotate(x):rotate(y);
		}
		rotate(x);
	}
}
void access(int x)
{
	int t=0;
	while(x)
	{
		splay(x);
		tr[x].rs=t;
		pushup(x);
		t=x,x=tr[x].f;
	}
}
void makeroot(int x)
{
	access(x);
	splay(x);
	swap(tr[x].ls,tr[x].rs);
	if(tr[x].ls) tr[tr[x].ls].lazy^=1;
	if(tr[x].rs) tr[tr[x].rs].lazy^=1;
}
void split(int x,int y)
{
	makeroot(x);
	access(y),splay(y);
}
int findroot(int x)
{
	access(x);
	splay(x);
	while(tr[x].ls) pushdown(x),x=tr[x].ls;
	splay(x);
	return x;
}
void link(int x,int y)
{
	makeroot(x);
	if(findroot(y)==x) return;
	tr[x].f=y;
}
void cut(int x,int y)
{
	if(findroot(x)!=findroot(y)) return;
	split(x,y);
	if(tr[x].f!=y||tr[x].rs) return;
	tr[x].f=tr[y].ls=0;
	pushup(y);
	return;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		int x;
		scanf("%d",&x);
		tr[i].sum=tr[i].val=x;
	}
	for(int i=1;i<=m;i++)
	{
		int opt,x,y;
		scanf("%d%d%d",&opt,&x,&y);
		if(opt==0)
		{
			split(x,y);
			printf("%d\n",tr[y].sum);
		}
		if(opt==1) link(x,y);
		if(opt==2) cut(x,y);
		if(opt==3) splay(x),tr[x].val=y;
	}
	return 0;
}

例题

1. [国家集训队] Tree II

难点在于有两个标记,加法和乘法的下放是有一定顺序的,下放乘法标记时原有的增量也会乘上这个数

所以每次可以先下放乘法,再处理加法,因为每个数都要加,所以还要另外记录 siz

最后把链分离出来输出和即可

2. [Wc2006]水管局长数据加强版

首先不强制在线,所以可以倒序处理变成加边

显然的,最后的答案一定在最小生成树上,所以要动态维护图的最小生成树

考虑新加入的边 \((u,v)\),如果未联通则直接建边,否则找到链上最大的边,判断大小关系,决定是否连边

因为是涉及边的问题,所以可以把每条边拆成点,维护最大值和位置即可

3. [Codechef MARCH14] GERALD07加强版

在一张没有环的图上,联通块个数就是 n- 边数

考虑我们新加进来一条边,此时如果这两个点已经连通,那么去掉环上的一条边联通情况不变

所以每一次可以删掉加入时间最早的边,因为要区间查询,所以主席树维护一下加到每一条边的时候每条边的存在情况即可

4.[bzoj3159]决战

难点在于反转,因为如果直接翻转这条链。那么子树的位置关系也会改变,这样就改了整颗子树,显然是不对的

如何做到只改这条链,可以类似文艺平衡树的方法,把修改的部分在 splay 上弄成一个区间,这样直接分裂下来打懒标记即可

所以可以对每个实链维护一个 splay,然后 access 操作时进行分裂和合并,两棵树同时操作即可

posted @ 2025-09-19 11:38  wangsiqi2010916  阅读(7)  评论(0)    收藏  举报