LCT


动态树问题:维护一个森林,支持删除某条边,加入某条边并保证时刻是一个森林,我们需要维护森林的一些信息,一般的操作有询问两点连通性,询问两点路径的权值和,修改某点的权值。

基本介绍

$LCT$是用来解决动态树问题的数据结构,各种操作的时间复杂度都是$O(log\ n)$的(不太会证),我们对整个森林进行虚实链剖分,并保证原树中每个节点最多只有一个儿子是通过实链连接的,如图所示,就是满足条件的一种情况(此时森林有两棵树)。

image-20220906135438509

然后我们用$splay$维护每条实链,并将这些$splay$称为辅助树(原树就是由虚实链构成的树),使$splay$的中序遍历就是该实链从最高点到最低点的路径。注意

  1. 每棵$splay$维护一条实链,$splay$中的节点不会记录与其通过虚边连接的儿子。
  2. 辅助树的各棵$splay$之间并不是孤立的,每棵$splay$的根节点会记录其在另一棵辅助树中的父亲(每个树中只有一棵$splay$的父亲为$0$)
  3. 原树中某点的父亲不等于辅助树中某点的父亲
  4. 辅助树可以在满足条件的情况下换根
  5. 辅助树的根节点的父亲“不认”这个根节点
  6. 原树中每个节点最多只有一个通过实边连接的儿子,这样才能保证每棵$splay$维护的都是一条链

下面抛出例题,通过此例题来讲解$LCT$的各种操作:

维护的信息

struct node{
    int s[2],p,v;//儿子、父亲、权值
    int sum,rev;//异或和、翻转标记,虽然本题无翻转操作,但LCT的基本操作要用到
}tr[N];

isroot

bool isroot(int u){//判断u是否是其所在splay的根
    return tr[tr[u].p].s[0]!=u&&tr[tr[u].p].s[1]!=u;
}

splay的函数

我们需要对原本的部分$splay$函数进行小小的修改。

pushup

//本题维护异或和
void pushup(int u){
    tr[u].sum=tr[tr[u].s[0]].sum^tr[tr[u].s[1]].sum^tr[u].sum;
}

pushdown

//经典的翻转操作
void pushrev(int u){
    swap(tr[u].s[0],tr[u].s[1]);
    tr[u].rev^=1;
}
void pushdown(int u){
    if (tr[u].rev){
        pushrev(tr[u].s[0]);
        pushrev(tr[u].s[1]);
        tr[u].rev=0;
    }
}

rotate

void rotate(int x){
    int y=tr[x].p,z=tr[y].p;
    int k=tr[y].s[1]==x;
    //唯一的不同处,辅助树根节点的父亲不能记录该根节点为儿子
    if(!isroot(y)) tr[z].s[tr[z].s[1]==y]=x;
    tr[x].p=z;
    tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;
    tr[x].s[k^1]=y,tr[y].p=x;
    pushup(y),pushup(x);
}

splay

//LCT中我们只用到了把点旋转到根的操作,因此只有一个参数
void splay(int x){//把x旋转到所在splay的根
    //我们要先把辅助树的根节点到该点路径上的懒标记下传
    static int stk[N];
    int tt=0,t=x;
    stk[++tt]=t;
    while(!isroot(t)) stk[++tt]=t=tr[t].p;
    while(tt) pushdown(stk[tt--]);
    while (!isroot(x)){
        int y=tr[x].p,z=tr[y].p;
        if(!isroot(y))
            if((tr[z].s[1]==y)^(tr[y].s[1]==x)) rotate(x);
            else rotate(y);
        rotate(x);
    }
}

access

//将x和其所在原树的根节点通过实边连起来,并把x旋转到辅助树的根
void access(int x){
    int z=x;//记录初始的节点编号
    //y初始为0,表示把x和它下面的点断开
    for(int y=0;x;y=x,x=tr[x].p){//x沿着虚边往上跳
        splay(x);//先转到当前辅助树的根
        tr[x].s[1]=y,pushup(x);//把上个树接到中序遍历后面
    }
    splay(z);//把初始的节点转到根
}

对于正确性,我们知道我们是从$x$节点往根跳,深度是递减的,因此$y$是$x$的右儿子,而我们也能时刻保证原树中每个节点只有一个实边儿子,因为我们把$x$旋转到辅助树的根后,它的儿子深度比它大,因此一定是$x$的右儿子,然后我们把$x$的右儿子修改为$y$,保证了每个点只有一个实边儿子。

makeroot

void makeroot(int x){//将x变为原树的根
    access(x);//此时x为辅助树的根节点,且右儿子为0
    pushrev(x);//翻转左右子树,此时x左儿子为0,因此x是深度最低的点,x是原树根节点
}

findroot

int findroot(int x){//找到x所在的原树的根节点,再将原树的根节点旋转到辅助树的根节点,此时根节点无左儿子
    access(x);//打通根节点到x的实链,当前x位于辅助树的根节点位置
    while(tr[x].s[0]) pushdown(x),x=tr[x].s[0];//找到辅助树中序遍历的第一个元素
    splay(x);//转到根节点
    return x;
}

split

void split(int x,int y){//将x到y的路径变为实边路径,并把y变为辅助树的根
    makeroot(x);//先把x设为原树的根
    access(y);//在打通根到y的实链,此时y是辅助树的根,y的sum就是x~y路径上的权值异或和
}
void link(int x,int y){//若x,y不连通,则加入(x,y)这条边
    makeroot(x);//先把x设为根
    if(findroot(y)!=x) tr[x].p=y;//如果不连通,在x、y间添加一条虚边,因为我们维护的是森林
}

cut

void cut(int x,int y){//若边x,y存在,则删掉x,y这条边
    makeroot(x);//把x变为原树的根,则此时y一定是x的右儿子
    //因为x,y可能并不通过实边连接,此时我们findroot(y)便能使其连通,同时x会被转到辅助树的根节点
    if(findroot(y)==x&&tr[x].s[1]==y&&!tr[y].s[0]){//保证辅助树中y是x的后继节点
        tr[y].p=tr[x].s[1]=0;
        pushup(x);
    }
}

这样,我们就讲完了$LCT$的基本操作,然后放上完整$AC$代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m;
#define ls tr[u].s[0] 
#define rs tr[u].s[1] 
struct node{
	int s[2],p,v;
	int sum,rev;
}tr[N];
bool isroot(int u){
	return tr[tr[u].p].s[0]!=u&&tr[tr[u].p].s[1]!=u;
}
void pushup(int u){
	tr[u].sum=tr[ls].sum^tr[u].v^tr[rs].sum;
}
void pushrev(int u){
	swap(tr[u].s[0],tr[u].s[1]);
	tr[u].rev^=1;
}
void pushdown(int u){
	if (tr[u].rev){
		pushrev(tr[u].s[0]);
		pushrev(tr[u].s[1]);
		tr[u].rev=0;
	}
}
void rotate(int x){
	int y=tr[x].p,z=tr[y].p;
	int k=tr[y].s[1]==x;
	if(!isroot(y)) tr[z].s[tr[z].s[1]==y]=x;
	tr[x].p=z;
	tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;
	tr[x].s[k^1]=y,tr[y].p=x;
	pushup(y),pushup(x);
}
void splay(int x){
	static int stk[N];
	int tt=0,t=x;
	stk[++tt]=t;
	while(!isroot(t)) stk[++tt]=t=tr[t].p;
	while(tt) pushdown(stk[tt--]);
	while (!isroot(x)){
		int y=tr[x].p,z=tr[y].p;
		if(!isroot(y))
			if((tr[z].s[1]==y)^(tr[y].s[1]==x)) rotate(x);
		else rotate(y);
		rotate(x);
	}
}
void access(int x){
	int z=x;
	for(int y=0;x;y=x,x=tr[x].p){
		splay(x);
		tr[x].s[1]=y,pushup(x);
	}
	splay(z);
}
void makeroot(int x){
	access(x);
	pushrev(x);
}
int findroot(int x){
	access(x);
	while(tr[x].s[0]) pushdown(x),x=tr[x].s[0];
	splay(x);
	return x;
}
void split(int x,int y){
	makeroot(x);
	access(y);
}
void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x) tr[x].p=y;
}
void cut(int x,int y){
	makeroot(x);
	if(findroot(y)==x&&tr[x].s[1]==y&&!tr[y].s[0]){
		tr[y].p=tr[x].s[1]=0;
		pushup(x);
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>tr[i].v;
	while(m--){
		int type,x,y;
		cin>>type>>x>>y;
		if(type==0){
			split(x,y);
			printf("%d\n",tr[y].sum);
		}
		else if(type==1){
			link(x,y);
		}
		else if(type==2){
			cut(x,y);
		}
		else{//修改的话,我们先把x转到根,然后单点修改并pushup即可
			splay(x);
			tr[x].v=y;
			pushup(x);
		}
	}
	return 0;
}

文章作者: verynewabie
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 verynewabie !
评论
  目录