【算法竞赛学习笔记】Link-Cut-Tree基础-超好懂的数据结构

本文介绍了Link-Cut-Tree(动态树)数据结构的基本概念、核心操作及其应用场景。包括链上求和、求最值、修改等操作,并通过具体实例展示了如何使用LCT解决实际问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


titile : Link-Cut-Tree
time : 2021-7-21
tags : ACM,数据结构
author : Linno


LCT

Link-Cut-Tree,中文名为动态树,是一种可以动态链信息、连通性、边权、子树信息等神奇的东西的数据结构。与树链剖分需要维护重边和轻边类似,LCT需要维护树的实边虚边的。一个结点最多连出一条向儿子的实边,因此实边会聚集成链。splay维护一条实链。

前置芝士

树链剖分

平衡树(splay即可)

适用范围

  • 链上求和

  • 链上求最值

  • 链上修改

  • 子树修改

  • 子树求和

(上述五种均可用树链剖分解决)

  • 换根
  • 断开树上的一条边
  • 连接两个点,保证连接后仍然是一棵树

性质

所有结点存在且仅存在于一个splay中

每个splay中的结点的深度按中序遍历顺序递增

概念

偏爱儿子:Preferred Child,最后一次被access过的儿子。

实边:Preferred Edge,连接父节点与偏爱儿子的边。

偏爱路径:Preferred Path,由实边及实边连接的结点构成的链。

辅助树:Auxiliary Tree,由一条偏爱路径上的所有节点所构成的Splay称作这条链的辅助树。

基本操作

pushup(x):上推操作,维护信息。

inline void pushup(int p){
    // maintain other variables
    siz[p] = siz[ls] + siz[rs] + 1;
}

pushdown(x):下推操作,维护信息。

inline void pushdown(int p){
    if (tag[p] != std_tag){
        // pushdown the tag
        tag[p] = std_tag;
    }
}

isroot(x):判断x是否为树根。

inline bool isroot(int x) { return to[fa[x]][0] != x && to[fa[x]][1] != x; }

get(x):对应splay中获取x是父亲的哪个儿子。

inline bool get(int x) { return to[fa[x]][1] == x; }

rotate(x):splay中的旋转操作,将x向上旋转一层。

void rotate(int x){
    int f = fa[x], ff = fa[f], k = get(x);
    if (!isroot(f)) to[ff][get(f)] = x;  //这句一定要写在前面,因为 isroot 的判断机制
    to[f][k] = to[x][k ^ 1], fa[to[x][k ^ 1]] = f;
    to[x][k ^ 1] = f, fa[f] = x, fa[x] = ff;
    pushup(f), pushup(x);
}

splay(x):将x旋转到树根。(详细见splay)

void splay(int x){
    update(x); //先下传标记,因为 splay 操作是自下向上的,上方的标记需要先 pushdown 下来
    for (int f = fa[x]; !isroot(x); rotate(x), f = fa[x])
        if (!isroot(f)) rotate(get(x) == get(f) ? f : x);
}
核心部分

access(x)

将原树上根到x中的所有点放在一条实链中,使根到x成为一条实链,并且在同一棵splay中。(x到根节点的路径变为偏爱路径)

算法流程:伸展x,把x的父亲的右儿子修改为下一个x,如果是第一次那就是0(虚边边实边,原右实儿子变虚儿子),对x的父亲重复上面步骤。

inline void access(int x){
	for(int y=0;x;x=fa(y=x)){
		splay(x);
		rs(x)=y;
		update(x);
	}
}

update(x)

在access操作后,递归地从上到下pushdown更新信息。

makeroot(x)

使x成为整个LCT的根。

void makeroot(int x) { access(x), splay(x), rev[x] ^= 1; }

link(x,y):在x点和y点之间加一条边。

算法流程:先将x变为根,然后直接连到轻边上去。

void link(int x, int y) { 
    makeroot(x);
    if(findroot(y)==x) return;
    fa(x) = y;
}

cut(x,y):删除x和y点之间的边。

算法流程:Markroot(x)->判断合法性->双向短边->更新信息

①如果保证合法,直接 split(x,y),此时原树上x为根,辅助树上y为根,那么x在辅助树上一定是y的左儿子,直接断开即可。

②如果不保证合法,需要满足三个条件:

x与y连通;x与y路径上没有其他链;x没有右儿子。x与y连通;x与y路径上没有其他链;x没有右儿子。xyxyx

三面三个条件保证x与y之间有边,需要用到find函数。

inline void cut(int x,int y){
    mkroot(x); //换根
    if(findroot(y)!=x||fa(y)!=x||ls(y)) return;
    fa(y)=rs(x)=0;
    update(x); //这是根据题目更新信息的函数
}

find:找到当前辅助树对应的原树的根

access(x) 后,再 splay(x),这样根就是该 Splay 中的最小值,根据"二叉搜索树"性质,一路走左儿子即可。注意过程中下推标记,注意最后把根 splay 上去保证复杂度。

int find(int x){
    access(x), splay(x);
    while (ls) pushdown(x), x = ls;
    return splay(x), x;
}

fix(x,k):将x的点权修改成k。

access(x), splay(x), val[x] = y, pushup(x);

split(x,y):拿出一颗splay维护x点与y点之间的路径,方便区间操作。

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

reverse(x):交换x的左右子树。

void reverse(int x,int y){
	swap(ls(x),rs(x)),tg(ls(x))^=1,tg(rs(x))^=1;
}

常用宏定义

#define ls(x) (spl[x].ch[0])
#define rs(x) (spl[x].ch[1])
#define fa(x) (spl[x].fa)
#define get(x,f) (rs(f)==x)
#define connect(x,f,s) spl[fa(x)=f].ch[s]=x
#define update(x) spl[x].res=spl[ls(x)].res^spl[rs(x)].res^spl[x].val
#define ntroot(x) (ls(fa(x))==x||rs(fa(x))==x) 
#define reverse(x) std::swap(ls(x),rs(x)),spl[x].tag^=1

复杂度分析

splay每次操作均摊复杂度为O(log2n)O(log_2n)O(log2n)

access复杂度也为O(log2n)O(log_2n)O(log2n)

所有lct的复杂度基本与access一致。

注意

①记得pushup和pushdown

②LCT中的rotate需要先判断isroot,所以连接父亲的父亲的关系的语句在最前面

③LCT中的splay就是旋转到根,不需要旋转到儿子

④建树时可以默认所有边为虚边,直接只指定父亲关系。

模板

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

给定 n 个点以及每个点的权值,要你处理接下来的 m 个操作。
操作有四种,操作从 0 到 3 编号。点从 1 到 n 编号。

  • 0 x y 代表询问从 x到 y 的路径上的点的权值的 xor 和。保证 x到 y 是联通的。
  • 1 x y 代表连接 x 到 y,若 xx 到 y已经联通则无需连接。
  • 2 x y 代表删除边 (x,y),不保证边 (x,y) 存在。
  • 3 x y 代表将点 x 上的权值变成 y。
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+7;

int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}
void write(int x){if(x>9) write(x/10);putchar(x%10+'0');}
void swap(int &x,int &y){x^=y^=x^=y;}

class LCT{
	private:
		#define LCT_SIZE N
		#define Pushup(x) (T[x].sum=T[x].val^T[T[x].ch[0]].sum^T[T[x].ch[1]].sum)
		#define Rever(x) (swap(T[x].ch[0],T[x].ch[1]),T[x].rev^=1)
		#define Pushdown(x) (T[x].rev&&(Rever(T[x].ch[0]),Rever(T[x].ch[1])),T[x].rev=0)
		#define Which(x) (T[T[x].fa].ch[1]==x)
		#define Connect(x,y,d) (T[T[x].fa=y].ch[d]=x)
		#define Isroot(x) (T[T[x].fa].ch[0]^x&&T[T[x].fa].ch[1]^x)
		#define Makeroot(x) (Access(x),Splay(x),Rever(x))
		#define Split(x,y) (Makeroot(x),Access(y),Splay(y))
		int Stack[LCT_SIZE];
		struct Tree{
			int val,sum,fa,rev,ch[2];
		}T[LCT_SIZE];
        inline void Rotate(int x){
            int fa=T[x].fa,pa=T[fa].fa,d=Which(x);
            !Isroot(fa)&&(T[pa].ch[Which(fa)]=x),T[x].fa=pa,Connect(T[x].ch[d^1],fa,d),Connect(fa,x,d^1),Pushup(fa),Pushup(x);
        }
        inline void Splay(int x){
            int fa=x,Top=0;
            while(Stack[++Top]=fa,!Isroot(fa)) fa=T[fa].fa;
            while(Top) Pushdown(Stack[Top]),--Top;
            while(!Isroot(x)) fa=T[x].fa,!Isroot(fa)&&(Rotate(Which(x)^Which(fa)?x:fa),0),Rotate(x);
        }
		inline void Access(int x){for(int son=0;x;x=T[son=x].fa) Splay(x),T[x].ch[1]=son,Pushup(x);}
		inline int Findroot(int x){Access(x),Splay(x);while(T[x].ch[0]) Pushdown(x),x=T[x].ch[0];return Splay(x),x;}
	public:
		inline void Init(int len,int *data){for(int i=1;i<=len;++i) T[i].val=data[i];}
		inline void Link(int x,int y){Makeroot(x),Findroot(y)^x&&(T[x].fa=y);}
		inline void Cut(int x,int y){Makeroot(x),!(Findroot(y)^x)&&!(T[y].fa^x)&&!T[y].ch[0]&&(T[y].fa=T[x].ch[1]=0,Pushup(x));}
		inline void Update(int x,int v){Splay(x),T[x].val=v;}
		inline int Query(int x,int y){return Split(x,y),T[y].sum;}
}LCT;

int n,m,op,x,y,a[N];

signed main(){
	n=read();m=read();
	for(int i=1;i<=n;++i) a[i]=read();
	LCT.Init(n,a);
	for(int i=1;i<=m;++i){
		op=read();x=read();y=read();
		if(op==0){
			write(LCT.Query(x,y));puts("");
		}else if(op==1){
			LCT.Link(x,y);
		}else if(op==2){
			LCT.Cut(x,y);
		}else if(op==3){
			LCT.Update(x,y);
		}
	}
	return 0;
} 
luoguP3203 [HNOI2010]弹飞绵羊

游戏一开始,Lostmonkey在地上沿着一条直线摆上n个装置,每个装置设定初始弹力系数ki,当绵羊达到第i个装置时,它会往后弹ki步,达到第i+ki个装置,若不存在第i+ki个装置,则绵羊被弹飞。绵羊想知道当它从第i个装置起步时,被弹几次后会被弹飞。为了使得游戏更有趣,Lostmonkey可以修改某个弹力装置的弹力系数,任何时候弹力系数均为正整数。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+7;

int ch[N][2],sz[N],fa[N];

inline bool nroot(int x){
	return ch[fa[x]][0]==x||ch[fa[x]][1]==x;
}

inline void pushup(int x){
	sz[x]=sz[ch[x][0]]+sz[ch[x][1]]+1;
}

inline void rotate(int x){
    int y=fa[x],z=fa[y],k=ch[y][1]==x,w=ch[x][!k];
    if(nroot(y))ch[z][ch[z][1]==y]=x;
	ch[x][!k]=y;ch[y][k]=w;
    if(w) fa[w]=y;fa[y]=x;fa[x]=z;
    pushup(y);
}

inline void splay(int x){
	int y,z;
	while(nroot(x)){
		y=fa[x];z=fa[y];
		if(nroot(y)) rotate((ch[y][0]==x)^(ch[z][0]==y)?x:y);
		rotate(x);
	}
	pushup(x);
}

inline void access(int x){
	for(int y=0;x;x=fa[y=x]) splay(x),ch[x][1]=y,pushup(x);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,m; 
	cin>>n;
	for(int i=1,k;i<=n;++i){
		sz[i]=1;
		cin>>k;
		if(i+k<=n) fa[i]=i+k; //弹飞不连边 
	} 
	cin>>m;
	for(int i=1,op,j,k;i<=m;++i){
		cin>>op;
		if(op==1){
			cin>>j;++j;
			access(j);splay(j); //直接查询 
			cout<<sz[j]<<"\n";
		}else{
			cin>>j>>k;++j;
			access(j);splay(j);
			ch[j][0]=fa[ch[j][0]]=0; //直接断边 
			if(j+k<=n) fa[j]=j+k;   //直接连边 
			pushup(j); 
		}
	}
	return 0;
} 
luoguP2147 [SDOI2008] 洞穴勘测

给定洞穴个数和指令个数,指令分为:连接两个洞穴、摧毁两个洞穴之间的通道、询问两个洞穴是否连通。

只有Link和Cut两种操作。以及优化LCT版本的splay写法。

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+7;
struct Node{
	int fa,ch[2];
	bool tg; 
}spl[N];

inline int read(){	int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=f*-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}

#define ls(x) (spl[x].ch[0])
#define rs(x) (spl[x].ch[1])
#define tg(x) (spl[x].tg)
#define fa(x) (spl[x].fa)
#define get(x) (rs(fa(x))==x)
#define reverse(x) swap(ls(x),rs(x)),tg(ls(x))^=1,tg(rs(x))^=1
#define isroot(x) (ls(fa(x))!=x&&rs(fa(x))!=x)

inline void pushdown(int rt){
	if(!rt||!tg(rt)) return;
	reverse(rt);
	tg(rt)^=1;
}

inline void rotate(int rt){
	int x=fa(rt),y=fa(x),k=get(rt);
	if(!isroot(x)){
		if(ls(y)==x) ls(y)=rt;
		else rs(y)=rt;
	}
	fa(rt)=y;fa(x)=rt;fa(spl[rt].ch[!k])=x;
	spl[x].ch[k]=spl[rt].ch[!k];
	spl[rt].ch[!k]=x;
}

inline void splay(int rt){ //优化伸展,适应LCT 
	int top=0,stk[N];
	stk[++top]=rt;
	for(int i=rt;!isroot(i);i=fa(i)) stk[++top]=fa(i);
	while(top) pushdown(stk[top--]);
	while(!isroot(rt)){
		int x=fa(rt),y=fa(x);
		if(!isroot(x)){
			if((ls(x)==rt)^(ls(y)==x)) rotate(rt);
			else rotate(x);
		}
		rotate(rt);
	}
}

inline void access(int x){
	for(int y=0;x;y=x,x=fa(x)){
		splay(x);
		rs(x)=y;
	}
}

inline void mkroot(int x){
	access(x);
	splay(x);
	tg(x)^=1;
}

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

inline int findroot(int rt){
	access(rt);
	splay(rt);
	while(ls(rt)) rt=ls(rt);
	return rt;
}

inline void link(int x,int y){
	mkroot(x);
	fa(x)=y;
}

inline void cut(int x,int y){
	split(x,y);
	ls(y)=fa(x)=0;
}

int n,m,u,v;
char op[10];

signed main(){
	n=read();m=read();
	for(int i=1;i<=m;i++){
		scanf("%s",op);
		u=read();v=read();
		if(op[0]=='Q')
			if(findroot(u)==findroot(v)) puts("Yes");
			else puts("No");
		else if(op[0]=='C') link(u,v);
		else cut(u,v);
	}
	return 0;
}
luogu P2387 [NOI2014] 魔法森林

给一个无向图,每条边有a和b两种精灵。如果携带的精灵大于等于边所需精灵则可通行,问1到n点最少需要带多少精灵?

动态加边可用LCT来完成,属于LCT的综合应用题了。

#include<bits/stdc++.h>
#define ls(x) t[x].son[0]
#define rs(x) t[x].son[1]
#define get(x) (rs(t[x].fa)==x)
#define isroot(x) (ls(t[x].fa)!=x && rs(t[x].fa)!=x)
using namespace std;

const int maxn=2e5+7,inf=1e9+7;

inline void read(int &data){
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=f*-1;
		ch=getchar();	
	}
	while(ch>='0'&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	data=x*f;
}

struct E{
	int from,to,a,b;
}e[maxn];

struct LCT{
	int son[2],fa,mx,id;
	bool mark;
}t[maxn];

int n,m,idnum,ans,w[maxn],st[maxn];

bool cmp(E x,E y){
	return (x.a==y.a)?x.b<y.b:x.a<y.a; //先对a排序再对b排序 
}

void pushup(int x){
	t[x].mx=w[x],t[x].id=x;
	if(ls(x)&&t[ls(x)].mx>t[x].mx) t[x].mx=t[ls(x)].mx,t[x].id=t[ls(x)].id;
	if(rs(x)&&t[rs(x)].mx>t[x].mx) t[x].mx=t[rs(x)].mx,t[x].id=t[rs(x)].id;
}

void rotate(int x){
	int f=t[x].fa,ff=t[f].fa,flag=get(x);
	t[x].fa=ff;
	if(!isroot(f)) t[ff].son[rs(ff)==f]=x;
	t[f].son[flag]=t[x].son[flag^1],t[t[x].son[flag^1]].fa=f;
	t[x].son[flag^1]=f,t[f].fa=x;
	pushup(f),pushup(x);
}

void pushmark(int x){
	if(t[x].mark){
		t[x].mark=0,t[ls(x)].mark^=1,t[rs(x)].mark^=1;
		swap(ls(x),rs(x));
	}
}

void splay(int x){
	int top=0,now=x;st[++top]=now;
	while(!isroot(now)) st[++top]=(now=t[now].fa);
	while(top) pushmark(st[top--]);
	while(!isroot(x)){
		int f=t[x].fa,ff=t[f].fa;
		if(!isroot(f))((rs(ff)==f)^(rs(f)==x))?rotate(x):rotate(f);
		rotate(x);
	}
}

void access(int x){ //将根到x的所有点放在实链上 
	for(int y=0;x;y=x,x=t[y].fa){
		splay(x);t[x].son[1]=y,pushup(x);
	}
}

void makeroot(int x){ //将x置为根 
	access(x);splay(x);t[x].mark^=1;pushmark(x);
}

int findroot(int x){ //原树上的根 
	access(x);
	splay(x);
	pushmark(x);
	while(ls(x)) pushmark(x=ls(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) t[x].fa=y;
}

bool check(int x,int y){ //查看是否在同一棵树中 
	makeroot(x);
	return findroot(y)!=x;
}

signed main(){
	read(n);read(m);ans=inf;
	for(int i=1;i<=m;i++){
		read(e[i].from);read(e[i].to);read(e[i].a);read(e[i].b);
	}
	sort(e+1,e+1+m,cmp); //按a从小到大排序 
	for(int i=1;i<=m;i++){ //用LCT维护边权b的最小生成树  
		w[i+n]=e[i].b;
		if(e[i].from==e[i].to)continue; //去重边
		if(check(e[i].from,e[i].to))  link(e[i].from,i+n),link(i+n,e[i].to);
		else{
			split(e[i].from,e[i].to); //splay打通边两端的路径 
			int now=t[e[i].to].id,maxb=t[e[i].to].mx;
			if(maxb<=e[i].b) continue; //这条边的b比之前选取的都大就不需要考虑了 
			splay(now),t[ls(now)].fa=t[rs(now)].fa=0;
			link(e[i].from,i+n),link(i+n,e[i].to);
		}
		if(!check(1,n)){ //若在同一个连通块了 
			split(1,n); 
			ans=min(ans,t[n].mx+e[i].a); //1到n的路径上最大b的最小值+当前a 
		}
	}
	if(ans==inf) puts("-1");
	else printf("%d\n",ans);
	return 0;
}

参考资料

https://blog.youkuaiyun.com/qq_36551189/article/details/79152612

https://zhuanlan.zhihu.com/p/346299929

https://www.luogu.com.cn/blog/Soulist/solution-p2387

https://www.cnblogs.com/chenxiaoran666/p/LCT.html

https://www.cnblogs.com/chenxiaoran666/p/LCT_problems.html

https://www.cnblogs.com/flashhu/p/9498517.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RWLinno

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值