LCT 学习笔记

\[\huge{\texttt{Link-Cut-Tree}} \]

前言

\(8.23\) 动工,开始填坑。

前置知识:\(\text{Splay}\)

本篇为 \(\texttt{LCT}\) 的基础内容,更多的题目及解析请查看 \(\texttt{LCT——应用篇}\)

\(\texttt{LCT}\) 介绍

\(\texttt{LCT}\),全称 \(\texttt{Link-Cut-Tree}\),是一个可以动态维护森林的数据结构,一般常见支持操作有「加边」「删边」「修改结点信息」「查询两点连通性」「查询路径信息和」等,是解决动态树上问题强而有力的工具。

辅助树

在学习「树链剖分」时,我们会采用将每条边划分成「轻边」和「重边」来保证复杂度,另外还有并不常见「长链剖分」。

\(\texttt{LCT}\),我们采用「实链剖分」的方式来操作,即,将每一条边划分为「实边」和「虚边」,每一个点当且仅当与一个儿子的的连边是实边,其余都是虚边。

并且注意,在 \(\texttt{LCT}\) 中,「虚边」与「实边」都是可以动态变换的。

如何做到动态变换呢?我们采用 \(\texttt{Splay}\) 维护一条从上到下,以实边串联的路径。这里的「从上到下」指的是深度严格递增,即不存在深度相同的节点。中序遍历一棵 \(\texttt{splay}\) 就相当于从上到下遍历每一个节点。

这其实也就可以类比「树链剖分」,对于每一条重链(也是从上到下),用一个线段树维护。

根据上述,我们可以得到,任何一点在且仅在一个 \(\texttt{splay}\) 中,并且在一个 \(\texttt{Splay}\) 中,深度最小的点肯定是从根节点出发一直往左子树走到的最后一个点。

辅助树有一个性质是,虚边父亲不认儿子,但儿子认父亲,即一个 \(\texttt{Splay}\) 的根认他的父亲,但他的父亲不认这个根。

通过这个性质,我们就可以判断一个节点是不是某一棵 \(\texttt{Splay}\) 的根。

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

意思是,他既不是他的父亲的左孩子,又不是他的右孩子,那显然他的父亲就不认他,他就是根。

当然我们的 \(\texttt{rotate}\) 也要稍微改一下:

inline void rotate(int x)
{
    int y=fa[x],z=fa[y],chk=get(x);
    if(!isroot(y)) ch[z][get(y)]=x;//加了这一句话
    ch[y][chk]=ch[x][!chk],fa[ch[x][!chk]]=y;
    ch[x][!chk]=y,fa[y]=x,fa[x]=z;
    push_up(y);
    push_up(x);
    return;
}

显然,如果 \(y\) 是根,那么旋转之后 \(x\) 的父亲 \(z\) 就不能认 \(x\) 作儿子。

\(\texttt{splay}\) 操作也是同理:

inline void splay(int x)
{
    update(x);
    for(register int f;f=fa[x],!isroot(x);rotate(x))
        if(!isroot(f)) rotate(get(f)==get(x)?f:x);
    return;
}

道理也很简单,只要旋转到根就不再旋转了;如果父亲 \(f\) 是根,那么只单旋一次 \(x\) 即可,否则视情况双旋。这一部分是 \(\texttt{Splay}\) 的内容,如果你感到困惑,那么请移步至 \(\texttt{Splay}\)

\(\texttt{update}\) 是从某一节点的根节点开始,不断下传信息,具体下传什么信息稍后会讲。

实际上,有了辅助树后,我们所有的操作都是在辅助树上进行的,只关心节点在原树的深度是否合法。

Access

光有一堆 \(\texttt{splay}\) 构成的辅助树是没有用处的,\(\texttt{LCT}\) 的威力还要靠 \(\texttt{access}\) 操作来体现。

注意,这是 \(\texttt{LCT}\) 里一个最重要的操作,其余所有操作都依赖于它

如果我们调用 \(\operatorname{access}(x)\),那么我们会在原树中打通一条从 \(x\) 到根的实链,并使得这些点恰好存在于一棵 \(\texttt{splay}\) 中。

实现起来很简单,我们用图来解释(懒得画图了,就用 \(\texttt{OI-Wiki}\) 的吧,不过可能需要加载一小会)。

我们现在有这么一棵树,加粗的为实边,虚线为虚边。

它的辅助树可能会长这样:

如果我们调用 \(\operatorname{access}(N)\),那么就会打通一条在原树中,\(A-N\) 的一条路径。

第一步,我们将当前节点 \(N\) 旋转至它所在的 \(\texttt{Splay}\) 的根,即我们调用 \(\operatorname{splay}(N)\),这时,\(L\) 先会左旋一下,然后 \(N\) 再右旋。

\(N,L,O\) 的关系就变成,\(L\)\(N\) 的左儿子,\(O\)\(N\) 的右儿子,但这种形态就不合法了,因为 \(L\) 连了两条实链,所以我们将 \(L\) 的右儿子设成 \(\text{NULL}\),即,将 \(L-O\) 的边改成了虚边。

那么,现在辅助树就变成了这样:

现在,我们找 \(N\) 的父亲 \(I\),再将 \(I\) 旋转至它所在的 \(\texttt{Splay}\) 的根。

注意这里的旋转,由于 \(I\) 并不认 \(N\) 这个儿子,所以 \(K\) 右旋之后 \(N\) 还是 \(I\) 的儿子,但是我们想打通 \(A-N\) 的路径,而 \(I\) 连的儿子是 \(K\),所以我们需要将 \(I-K\) 的实边替换成虚边,\(I-N\) 的虚边替换成实边。其实也就是将 \(I\) 的右儿子更改成 \(N\)

现在,辅助树就会变成这样:

之后的操作,其实也就顺藤摸瓜就行了。

我们再找到 \(I\) 的父亲 \(H\),然后将 \(H\) 旋转至 \(H\) 所在 \(\texttt{Splay}\) 根,再将 \(H\) 的右儿子设成 \(I\)

再找到 \(H\) 的父亲 \(A\),将 \(A\) 旋转至 \(A\) 所在的 \(\texttt{Splay}\) 的根,然后将 \(A\) 的右儿子设成 \(H\)

现在,我们发现 \(A\) 没有父亲了,那么停止操作。

验证一下,我们中序遍历\(A\) 为根的那棵 \(\texttt{Splay}\),会得到一条 \(A-C-G-H-I-L-N\) 的路径。

原图再放一遍:

恰好就是 \(A-C-G-H-I-L-N\)

如果你仔细读完上面的论述,相信你很快就能找到规律:

  1. 设上一个 \(\texttt{Splay}\) 的根节点为 \(p\),设 \(x\)\(p\) 的父亲。一开始,\(x\) 为给定的点,\(p\)\(\text{NULL}\)

  2. \(x\) 不为空,则将 \(x\) 旋转至 \(x\) 所在 \(\texttt{splay}\) 的根节点,然后将 \(x\) 的右儿子设为 \(p\);否则退出。

  3. \(p\leftarrow x\)\(x\leftarrow\text{father}(x)\),重复 \(2\) 操作。

代码也就跃然纸上了:

inline void access(int x)
{
    for(register int p=0;x;p=x,x=fa[x])
    {
        splay(x);
        ch[x][1]=p;
        push_up(x);//更新了儿子之后注意 push_up 节点信息
    }
    return;
}

这里 \(\texttt{push\_up}\) 什么信息需要视题目而定,比如更新子树和,子树大小,子树异或和之类的。

那么 \(\texttt{access}\) 操作有什么用呢?来看看它可以实现的函数!

makeroot

这也是 \(\texttt{LCT}\) 中一个非常重要的操作,目的是,将指定的点 \(x\) 旋转至原树的根。

我们先打通 \(x\) 到树根的路径,即调用 \(\operatorname{access}(x)\),现在 \(root\to x\) 路径上所有的点都在一棵 \(\texttt{splay}\) 里了,我们把 \(x\) 旋转到根上,即调用 \(\operatorname{splay}(x)\)

可能有些读者会觉得,\(x\) 已经旋转到根了,\(\texttt{makeroot}\) 操作就做完了。

但是,树的形态是否发生改变,取决于 \(\texttt{Splay}\) 的中序遍历是否发生改变。

冷静思考一下。我们发现,\(x\) 是原树中,\(root\to x\) 这条路径上深度最深的点,也就是当 \(x\) 旋转至根时,\(x\) 没有右子树,现在我们想让 \(x\) 变成根,就相当于中序遍历发生反转!也就是将 \(x\) 为根的这棵 \(\texttt{Splay}\) 进行反转操作,即不断交换左儿子和右儿子,这样中序遍历就能反转。

那么我们直接对 \(x\) 节点打一个标记并反转它的左右儿子不就好了?并且这也解释了 \(\operatorname{update}\) 函数里的 \(\operatorname{push\_down}\) 到底下传的是什么信息。

代码同样非常简单。先给出 \(\operatorname{push\_down}\) 的代码:

inline void push_down(int u)
{
    if(tag[u])
    {
        if(ch[u][0]) lazy_tag(ch[u][0]);
        if(ch[u][1]) lazy_tag(ch[u][1]);
        tag[u]=false;
    }
    return;
}

\(\operatorname{lazy\_tag}\) 函数也就是一个打标记:

inline void lazy_tag(int u)
{
    Swap(ch[u][0],ch[u][1]);
    tag[u]^=1;
    return;
}

再稍回顾一下刚刚在 \(\operatorname{splay}\) 函数中的 \(\operatorname{update}\) 函数,它的目的是,从 \(x\) 所在 \(\texttt{Splay}\) 的根开始下传信息。

那么我们就一直找父亲,直到找到根,然后 \(\operatorname{push\_down}\)

inline void update(int u)
{
    if(!isroot(u)) update(fa[u]);
    push_down(u);
    return;
}

\(\operatorname{makeroot}\) 函数至此也就很容易实现了:

  1. 打通路径。
  2. 旋转至根。
  3. 翻转。

注意,这里的翻转并不是真正意义上的翻转,而是指打一个翻转的标记。

inline void makeroot(int x)
{
    access(x);
    splay(x);
    lazy_tag(x);
    return;
}

有了 \(\operatorname{access}\)\(\operatorname{makeroot}\),我们就可以实现一些函数啦!

findroot

字面意思,找到某一点所在原树的根。

这个根并查集很像,比如判断两点的连通性,我们就可以判断它们所在树的树根是否一样。

我们先打通到根的路径,然后把询问点 \(x\) 旋转至根。

这里又用到了辅助树的性质,\(x\) 是在原树中 \(root\to x\) 深度最大的点,所以旋转之后的 \(\text{Splay}\)\(x\) 一定没有右子树。换句话说,树根一定是 \(x\) 所在 \(\text{Splay}\) 中中序遍历最小的节点,也就是不断的找左子树。

注意我们在暴力找左子树的时候要下传信息,代码同样非常简单:

inline int findroot(int x)
{
    access(x);
    splay(x);
    push_down(x);
    while(ch[x][0]) x=ch[x][0],push_down(x);
    return x; 
}

Link & Cut

这也是 \(\texttt{LCT}\) 中最常见的两个操作,假设操作的两点分别为 \(x,y\)

先来说 \(\texttt{Link}\),先判断一手是否合法,即 \(x,y\) 是否已经在同一个连通块里了,这个可以通过 \(\operatorname{findroot}\) 轻松实现。

否则我们直接指定 \(x\) 变成树根,即 \(\operatorname{makeroot}(x)\),然后将 \(x\) 的父亲设成 \(y\),就类似并查集。注意,此时我们钦定 \(x\to y\) 的边为虚边。

代码:

inline void link(int x,int y)
{
    if(!same(x,y)) makeroot(x),fa[x]=y;
    return;
}

\(\operatorname{same}(x,y)\) 判断的是 \(x,y\)\(\operatorname{findroot}\) 返回值是否相等)

再来看 \(\texttt{cut}\),这个同样需要用到辅助树的性质。

\(\operatorname{makeroot}(x)\),然后打通根到 \(y\) 也就是 \(x\to y\) 的路径,然后将 \(y\) 旋转到根上。

对于判断合法性,如果 \(x,y\) 之间有边,那么:

  1. \(x\) 一定是 \(y\) 的左儿子。
  2. \(x\) 没有右儿子。

先来解释第一点。还是根据辅助树的性质,\(y\)\(x\to y\) 路径上最后一个点,所以当 \(y\) 旋转到根上的时候,\(y\) 没有右子树。

再来解释第二点,如果 \(x\) 有右儿子 \(z\),根据辅助树的性质,原树的形态是通过 \(\text{Splay}\) 中序遍历得来的,那么 \(x\) 肯定在 \(y\) 之前,\(z\)\(x\) 之后且在 \(y\) 之前,所以一定会出现一条 \(x\to z\to y\) 的路径,而树是一张无向无环图,所以,不可能存在一条 \(x\to y\) 的边。

判断完这两点之后,我们直接将 \(x\) 的父亲设为 \(\text{NULL}\)\(y\) 的左二子也设为 \(\text{NULL}\)

\(y\) 的左孩子更改,需要 \(\operatorname{push\_up}\) 信息。

代码也很容易:

inline void cut(int x,int y)
{
    split(x,y);
    if(ch[y][0]==x&&!ch[x][1]) ch[y][0]=fa[x]=0;
    push_up(y);
    return;
}

split

通常,根据题目需要,我们会提取出树上的一条链来打标记或做相应的操作。

假设我们要 \(\operatorname{split}(x,y)\),做法跟前面的其实很相像。

\(\operatorname{makeroot}(x)\),然后 \(\operatorname{access}(y)\),最后 \(\operatorname{splay}(y)\)。那么以 \(y\) 为根的 \(\text{Splay}\) 就恰好包含了 \(x\to y\) 路径上的所有点。

原理很简单,这里作为一个小问题留给读者思考。

代码

至此,\(\texttt{Link-Cut-Tree}\) 的基础内容都已经解决了,看一下模板题的代码吧:

#include<cstdio>
#include<algorithm>
#include<cmath>
#include<iostream>
#include<set>
#include<vector>
#include<queue>
#include<stack>
#include<cstring>
#include<cstdlib>
#include<ctime>
#define rep(i,a,b) for(register int i=a;i<=b;++i)
#define rev(i,a,b) for(register int i=a;i>=b;--i)
#define gra(i,u) for(register int i=head[u];i;i=edge[i].nxt)
#define Clear(a) memset(a,0,sizeof(a))
#define yes puts("YES")
#define no puts("NO")
using namespace std;
typedef long long ll;
const int INF(1e9+10);
const ll LLINF(1e18+10);
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9')s=s*10+(ch-'0'),ch=getchar();
	return s*w;
}
template<typename T>
inline T Min(T x,T y){return x<y?x:y;}
template<typename T>
inline T Max(T x,T y){return x>y?x:y;}
template<typename T>
inline void Swap(T&x,T&y){T t=x;x=y;y=t;return;}
template<typename T>
inline T Abs(T x){return x<0?-x:x;}

const int MAXN(3e5+10);

int n,m;

struct Link_Cut_Tree
{	
	int fa[MAXN],ch[MAXN][2],val[MAXN],sum[MAXN],siz[MAXN];
	bool tag[MAXN];
	
	inline void push_up(int u)
	{
		int ls=ch[u][0],rs=ch[u][1];
		sum[u]=sum[ls]^sum[rs]^val[u];
		siz[u]=siz[ls]+siz[rs]+1;
		return;
	}
	
	inline void lazy_tag(int u)
	{
		Swap(ch[u][0],ch[u][1]);
		tag[u]^=1;
		return;
	} 
	
	inline void push_down(int u)
	{
		if(tag[u])
		{
			if(ch[u][0]) lazy_tag(ch[u][0]);
			if(ch[u][1]) lazy_tag(ch[u][1]);
			tag[u]=false;
		}
		return;
	}
	
	inline bool get(int u){return u==ch[fa[u]][1];}
	
	inline bool isroot(int u){return u!=ch[fa[u]][0]&&u!=ch[fa[u]][1];}
	
	inline void update(int x)
	{
		if(!isroot(x)) update(fa[x]);
		push_down(x);
		return;
	}
	
	inline void rotate(int x)
	{
		int y=fa[x],z=fa[y],chk=get(x);
		if(!isroot(y)) ch[z][get(y)]=x;
		ch[y][chk]=ch[x][!chk],fa[ch[x][!chk]]=y;
		ch[x][!chk]=y,fa[y]=x,fa[x]=z;
		push_up(y);
		push_up(x);
		return;
	}
	
	inline void splay(int x)
	{
		update(x);
		for(register int f;f=fa[x],!isroot(x);rotate(x))
			if(!isroot(f)) rotate(get(f)==get(x)?f:x);
		return;
	}
	
	inline void access(int x)
	{
		for(register int p=0;x;p=x,x=fa[x])
		{
			splay(x);
			ch[x][1]=p;
			push_up(x);
		}
		return;
	}
	
	inline void makeroot(int x)
	{
		access(x);
		splay(x);
		lazy_tag(x);
		return;
	}
	
	inline int findroot(int p)
	{
		access(p);
		splay(p);
		push_down(p);
		while(ch[p][0]) p=ch[p][0],push_down(p);
		splay(p);
		return p;
	}
	
	inline void link(int x,int y)
	{
		if(findroot(x)!=findroot(y)) makeroot(x),fa[x]=y;
		return;
	}
	
	inline void cut(int x,int y)
	{
		makeroot(x);
		access(y);
		splay(y);
		if(ch[y][0]==x&&!ch[x][1]) ch[y][0]=fa[x]=0;
		return;
	}
	
	inline void split(int x,int y)
	{
		makeroot(x);
		access(y);
		splay(y);
		return;
	}
};
Link_Cut_Tree lct;

int main()
{
//	freopen("read2.txt","r",stdin);
	n=read(),m=read();
	rep(i,1,n) lct.val[i]=read();
	rep(i,1,m)
	{
		int opt=read(),x=read(),y=read();
		if(opt==0)
		{
			lct.split(x,y);
			printf("%d\n",lct.sum[y]);
		} 
		else if(opt==1) lct.link(x,y);
		else if(opt==2) lct.cut(x,y);
		else if(opt==3)
		{
			lct.splay(x);
			lct.val[x]=y;
		}
	}
	return 0;
}

\(\texttt{PS}\)\(\text{590ms}\)\(\text{5.58MB}\)\(\text{3.33KB}\)

完结撒花

感谢您的阅读!

或者继续前往 \(\texttt{LCT——应用篇}\)

posted @ 2022-08-30 07:29  UperFicial  阅读(352)  评论(0)    收藏  举报