LCT 学习笔记

前言

树链剖分中的实链剖分。前面已经讲过重链剖分和长链剖分了,实链剖分也早学了,来补上blog。

LCT,全名 Link-Cut Tree,是一个非常强大的维护两点之间的路径信息的数据结构,相比于重链剖分只能维护静态的一棵树,实链剖分可以维护动态的森林,且复杂度并没有什么改变。但是这玩意比重链剖分难学难写难理解,所以能简化不用是最好的。
LCT 基于 Splay 树,但在具体操作有些不同。在 LCT 中,每一条链都是一棵 Splay 树且没有明确的父子关系,依靠 Splay 强大性质,复杂度仍然可以保持良好。

这里以 luogu 模板题 P3690 为例。

关联元素:

chch0 为左儿子节点,ch1 为右儿子节点。
fafaii 的父节点。
valvalii 的权值。
sumsumi 为子树 i 的权值。
tagtagii 的翻转懒标记。

一些准备:

ls(x)x 的左儿子节点。

rs(x)x 的右儿子节点。

fa(x)x 的父节点。

notroot(x) x 是不是该 splay 树的根节点。
具体实现形式为:(fa(ls(x))==x||fa(rs(x))==x)
即节点 x 是不是父节点的左儿子或右儿子。(LCT 中一个节点只有两个儿子,这三个节点一定在同一棵 splay 树。)

get(x) 判断 x 是否是父节点的右儿子。

函数:

  1. pushup

对于该节点的更新。

void pushup(int x)
{
	tr[x].sum=tr[ls(x)].sum^tr[x].v^tr[rs(x)].sum;
}
  1. pushdown

懒标记的下转和更新。

void pushdown(int x)//下传懒标记
{
	if(tr[x].tag)
	{
		swap(ls(x),rs(x));//翻转
		tr[ls(x)].tag^=1;
		tr[rs(x)].tag^=1;
		tr[x].tag=0;
	}
}

注意:这种写法会认为上传信息时左右儿子的顺序是无关的,若有关时,在 pushuppushdown 即可。
3. pushall

对当前链懒标记全部下传,用于 splay 前调整树的结构。


void pushall(int x)//全部下传
{
	if(notroot(x))pushall(fa(x));
	pushdown(x);
}
  1. rotate

splay 中的旋转操作,但小做改动。

void rotate(int x)//旋转
{
	int y=fa(x),z=fa(y);
	int k=get(x);
	if(notroot(y))//不改变虚边
		tr[z].c[get(y)]=x;
	fa(x)=z;
	tr[y].c[k]=tr[x].c[k^1];
	fa(tr[x].c[k^1])=y;
	tr[x].c[k^1]=y;
	fa(y)=x;
	pushup(y),pushup(x);//先 y 后 x(y 已经是 x 的儿子了)
}
  1. splay

旋转至 splay 的根节点。

void splay(int x)//保证复杂度,并旋转至根节点
{
	pushall(x);//先调整树的结构
	while(notroot(x))
	{
		int y=fa(x);
		if(notroot(y))
			(get(x)^get(y))?rotate(x):rotate(y);
		rotate(x);
	}
}
  1. access

x 与当前整棵树的根节点之间的路径单独成一棵 splay 树。
LCT 的关键操作。

void access(int x)//重组实链,打通从x到全树根节点的链,全部改成实边,链上原有的其他边改为虚边
{
	for(int y=0;x;y=x,x=fa(x))
	{
		splay(x);//先旋转至根
		rs(x)=y;//原右儿子改为虚边,y改为实边
		pushup(x);
	}
}
  1. makeroot

使 x 节点成为整棵树的根。

void makeroot(int x)
{
	access(x);
	splay(x);
	tr[x].tag^=1;//这里对于根到 x 的路径,深度是反转的,加上中序遍历深度递增的性质,做一次翻转子树即可维护
}
  1. findroot

找到整棵树的根节点。

int findroot(int x)
{
	access(x);
	splay(x);
	while(ls(x))
		pushdown(x),x=ls(x);
	splay(x);//这里视情况决定写不写,主要用于防止一条长链来回搜,毒瘤出题人会在这里卡常
	return x;
}
  1. split

分离出 xy 的一条链。

void split(int x,int y)
{
	makeroot(x);
	access(y);
	splay(y);
}
  1. check

这个函数为个人爱好,判断 xy 在不在同一个联通块里。

bool check(int x,int y)
{
	makeroot(x);
	return findroot(y)==x;
}
  1. link

连接 xy

bool link(int x,int y)
{
	if(check(x,y))
		return false;
	fa(x)=y;
	return true;
}
  1. cut

切断 xy

bool cut(int x,int y)
{
	if(!check(x,y)||ls(y)||fa(y)!=x)
		return false;
	rs(x)=fa(y)=0;
	return true;
}
  1. modify

单点修改。

void modify(int x,int c)
{
	splay(x);
	tr[x].val=c;
	//or do something...
	pushup(x);
}
  1. query

询问 xy 的链的信息。

int query(int x,int y)
{
	split(x,y);
	return tr[y].sum;
}

复杂度

空间复杂度 O(n),时间复杂度 splay 一次的复杂度均摊至 O(logn),总复杂度为调用 splay 次数 T×O(logn),即 O(Tlogn)。但大多数操作里只调用一两次,约为询问次数 q×O(logn),即 O(qlogn)

Code:

模板 LCT

完整代码:

namespace Lofty
{
	namespace LCT
	{
		struct trnode
		{
			int ch[2],fa,val,sum;//ch:儿子,fa:父亲,val:当前点的权值,:sum:以当前点为根的树的权值
			int tag;//懒标记
		}tr[N];
		#define ls(x) tr[x].ch[0]
		#define rs(x) tr[x].ch[1]
		#define fa(x) tr[x].fa
		#define notroot(x) (ls(fa(x))==x||rs(fa(x))==x)
		#define get(x) (rs(fa(x))==x)
		void pushup(int x)
		{
			tr[x].sum=tr[ls(x)].sum^tr[x].val^tr[rs(x)].sum;//左儿子异或当前点权值异或右儿子
		}
		void pushdown(int x)//下传懒标记
		{
			if(tr[x].tag)
			{
				swap(ls(x),rs(x));//翻转
				tr[ls(x)].tag^=1;
				tr[rs(x)].tag^=1;
				tr[x].tag=0;
			}
		}
		void pushall(int x)//全部下传
		{
			if(notroot(x))
				pushall(fa(x));//懒标记一般在根节点处,要向上找并下传
			pushdown(x);
		}
		void rotate(int x)//旋转
		{
			int y=fa(x),z=fa(y);
			int k=get(x);
			if(notroot(y))//不改变虚边
				tr[z].ch[get(y)]=x;
			fa(x)=z;
			tr[y].ch[k]=tr[x].ch[k^1];//连边
			fa(tr[x].ch[k^1])=y;
			tr[x].ch[k^1]=y;//连边
			fa(y)=x;
			pushup(y),pushup(x);//先y后x(y已经是x的儿子了)
		}
		void splay(int x)//保证复杂度,并旋转至根节点
		{
			pushall(x);
			while(notroot(x))//还没转到根节点
			{
				int y=fa(x);
				if(notroot(y))//尝试能否转两次
					(get(x)^get(y))?rotate(x):rotate(y);//如果y和x对于父亲而言都是同一方向的儿子,要先转y,才能保证y的另一个儿子是正确的,否则y可能成为一条链?
				rotate(x);
			}
		}
		void access(int x)//重组实链,打通从x到全树根节点的链,全部改成实边,链上原有的其他边改为虚边
		{
			for(int y=0;x;y=x,x=fa(x))
			{
				splay(x);//先旋转至根
				rs(x)=y;//原右儿子改为虚边,y改为实边
				pushup(x);
			}
		}
		void makeroot(int x)//将x换到全树的根节点,把树倒过来
		{
			access(x);//先打通,才能在同一棵splay树里
			splay(x);//已经在一棵splay树里了,直接旋转到根节点
			tr[x].tag^=1;//翻转,保证中序遍历深度是递增的
		}
		void split(int x,int y)//将x和y的路径与其他路径分离
		{
			makeroot(x);//先成为根节点,后面才能打通y到x的路径
			access(y);//打通y到根节点(x)的路径
			splay(y);
		}
		int findroot(int x)//找到根节点
		{
			access(x);//先打通
			splay(x);//旋转到根节点
			while(ls(x))//这时因为中序遍历的深度是递增的,我们只要找左儿子就可以找到深度最小的节点,那就是根节点
				pushdown(x),x=ls(x);
			splay(x);//防止卡一条链来回搜
			return x;
		}
		bool check(int x,int y)
		{
			makeroot(x);
			return findroot(y)==x;
		}
		bool link(int x,int y)//连边
		{
			if(check(x,y))
				return false;//如果已经是同一棵splay树了,那先前已经让x成为splay树的根了,找到的根就应是x,那就不应该连
			fa(x)=y;//y不是x所在的splay树,而splay树的根节点只能向另一个splay树的节点连一条虚边
			return true;
		}
		bool cut(int x,int y)//断边
		{
			if(!check(x,y)||fa(y)!=x||ls(y))
				return false;//不在同一棵splay树上或者没有直接相连,又或者y不是x的后继,中序遍历中还有其他节点
			rs(x)=fa(y)=0;//断开边
			pushup(x);//少了个儿子,要更新上传,权值会改变
			return true;
		}
		void modify(int x,int c)//更改权值
		{
			splay(x);//先转到根节点,不要影响了其他节点
			tr[x].val=c;
			pushup(x);//节点权值改了,也要更新上传,权值会改变
		}
		int query(int x,int y)//输出
		{
			split(x,y);
			return tr[y].sum;//信息保存在splay树的根节点
		}
		//以上就是模板LCT,实质上是将树拆成许多条实链,用splay维护,这两玩意儿都比较难理解
		//时间复杂度O(mlogn) 
	}
	void work()
	{
		int T=1;
		// read(T);
		while(T--)
		{
			int n,m;read(n,m);
			for(int i=1;i<=n;i++)
				read(LCT::tr[i].val);
			while(m--)
			{
				int op;read(op);
				switch(op)
				{
					int x,y;
					case 0:
						read(x,y);
						writeln(LCT::query(x,y));
						break;
					case 1:
						read(x,y);LCT::link(x,y);
						break;
					case 2:
						read(x,y);LCT::cut(x,y);
						break;
					case 3:
						read(x,y);LCT::modify(x,y);
						break;
				}
			}
		}
	}
}
posted @   wmtl_lofty  阅读(12)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示