我的坟头应该开满玫瑰吗?|

YYYmoon

园龄:1年粉丝:20关注:41

LCT

有一类问题,要求我们维护一个森林,支持加边和删边操作,并维护树上的一些信息。这类问题称为动态树问题。

Link-Cut Tree (LCT),就是用于解决动态树问题的数据结构。均摊复杂度 O(nlogn) .

LCT 支持的操作:查询/修改链上的信息,换根,动态连边/删边,合并两棵树/分离一棵树,动态维护连通性,等等

前置知识:Splay dalao blog

请奆佬们自行跳过这一部分,(蒟蒻因为太菜,不会Splay,写给自己看

Splay树,或 伸展树,是一种平衡二叉查找树。它通过 Splay/伸展操作 不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,能够在均摊 O(logn) 的时间内完成插入、查找和删除操作,并保持平衡而不至于退化成链。(时间复杂度证明需要势能分析,故摆烂)

二叉查找树的性质:左子树任意节点的值 < 根节点的值 < 右子树任意节点的值

基本操作

  • pushup(x): 改变节点位置后,更新节点信息

  • get(x): 判断节点 x 是父亲节点的左儿子还是右儿子

  • clear(x): 销毁节点 x

rotate 旋转操作

为了使 Splay 保持平衡。旋转的本质是将某个节点上移一个位置。

需要保证:

  • 整棵 Splay 的中序遍历不变(二叉查找树的性质

  • 受影响的节点维护的信息依然正确有效

  • root必须指向旋转后的根节点

具体分为左旋和右旋(图源oi-wiki

以右旋为例,设要旋转的节点为x,其父亲节点为y:

  1. 将y的左儿子指向x的右儿子,且x的右儿子的父亲指向y

  2. 将x的右儿子指向y,且y的父亲指向x

  3. 如果原来的y还有父亲z,那么把z的某个儿子指向x,且x的父亲指向z

void rotate(int x){//本质是交换x,y的位置 
	int y=t[x].fa;//x父亲 
	int z=t[y].fa;//x祖父
	int k=(t[y].c[1]==x);//x为0左1右儿子
	t[y].c[k]=t[x].c[k^1];
	if(t[x].c[k^1]) t[t[x].c[k^1]].fa=y;
	t[x].c[k^1]=y;
	t[y].fa=x,t[x].fa=z;
	if(z) t[z].c[t[z].c[1]==y]=x;
	pushup(y),pushup(x);
}

Splay 伸展操作

Splay操作规定:每访问一个节点x后,都要强制将其旋转到根节点

Splay操作,就是把x旋转到根的操作。定义y为x的父节点,z为y的父节点。Splay步骤有三种,具体分为六种情况。

  1. y是根节点:x是y左儿子,右旋;x是y右儿子,左旋。

  2. y不是根,且x、y同为左儿子或右儿子:同为左儿子,两次右旋;同为左儿子,两次左旋。这里都是先做y-z,再做x-y。

  3. y不是根,且x、y一个为左儿子,一个为右儿子:x是y左儿子,y是z右儿子,先对x-y右旋,再对x-z左旋;x是y右儿子,y是z左儿子,先对x-y左旋,再对x-z右旋。

void splay(int x){
	while(t[x].fa){
		int y=t[x].fa,z=t[y].fa;
		if(t[z].fa){
			if((t[z].c[0]==y)^(t[y].c[0]==x)) rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
	root=x;
}

好了,会了这俩操作LCT就够用了,所以Splay剩下操作不学了

实链剖分

众所周知,重链剖分就是根据子树大小把儿子节点分为重儿子和轻儿子,连出重链和轻链。它可以将树上的任意一条路径划分成不超过 O(logn) 条连续的链,从而达到维护树上路径的问题。

而对于这样动态树,我们想相应地通过一些手段来维护树上路径,就诞生了实链剖分。

我们对每个节点自行指定实儿子和虚儿子,(需要注意的是一个点不一定必须有实儿子),然后我们就可以利用 Splay 去维护每一条链。

辅助树(AuxTree

维护每条链的Splay之间通过某种方式相连形成的树结构。因为Splay维护的是每一条实链,而辅助树维护的就是一棵树。一些辅助树放在一起就是LCT,用于维护整个森林。

1.辅助树由多个Splay组成,每个 Splay 维护原树的一条实链,且中序遍历 Splay 对应实链从上到下的点,注意 Splay 中不能出现深度相同的点。

2.每个节点包含且仅包含于一个 Splay 中。

3.辅助树上边分为实边和虚边,实边包含在 Splay 中;虚边由该 Splay 中中序遍历最靠前的点x,也就是实链的链顶,指向它在原树中的父亲y=fa[x]。同时对于y,我们仍然让它在辅助树上的儿子为空,以表示这条边是虚边。所有虚边都认父不认子。

  1. 原树上的操作均可转化为辅助树上的操作。

整理一下原树和辅助树的关系:

  • 原树的实链都在辅助树的同一个Splay中

  • 原树的虚边由儿子所在的Splay的根节点指向父亲,但这个父亲不指向该根节点(不回指,认父不认子)

  • Splay上最多有两个实儿子,但可能有很多虚儿子

  • 原树的根不等于辅助树的根,原树的父亲指向不等于辅助树上的父亲指向

举例:一棵原树

它的辅助树就可以长成

这里我们可以更深刻地认识一下LCT虚边认父不认子的特性。即,从某个节点出来的虚边,在原树中是多少,无论辅助树长什么样,这个节点出发的虚边都还是多少。而虚边的子节点比较自由,指向的可以是子节点属于的splay中的任意一个点。

这让我们不由得考虑,辅助树是如何维护原树的节点关系的,尤其是对于虚边的子节点。其实是依靠我们规定的Splay中序遍历深度严格递增的性质。在后面的一系列操作中,这个性质都不会被破坏。

相关Splay的操作

get

寻找当前节点是父亲的哪个儿子

int get(int x){
	return t[t[x].fa].c[1]==x;
} 

isroot

用于判断该点是不是Splay的根。由于辅助树上虚边认父不认子的性质,不能直接看有没有father。

bool isroot(int x){
	return t[t[x].fa].c[0]!=x&&t[t[x].fa].c[1]!=x;
}

pushup/pushdown

在LCT的Splay中,基本上都要实现区间翻转操作,故需要下放懒标记。

正常的Splay上传下放操作

void pushup(int x){
	t[x].sum=t[t[x].c[0]].sum^t[t[x].c[1]].sum^t[x].val;
}
void pushdown(int x){
	if(t[x].tag){
		swap(t[x].c[0],t[x].c[1]);
		t[t[x].c[0]].tag^=1;
		t[t[x].c[1]].tag^=1;
		t[x].tag=0;
	}
}

但这种pushdown的写法会导致某个点上面有懒标记时,它的两个儿子还是反的,在某些题目中会导致错误。

可以理解为,每个节点储存的信息实际上就是它两个儿子的左右位置,所以一定要保证不论当前节点含不含懒标记,只要当前节点之上的节点的懒标记全部被下放,它储存的信息一定要是对的。(这和普通线段树对懒标记的要求就一样了)

所以更稳妥的写法是标记该节点的两个儿子需不需要翻转。

void pushrev(int x){
	swap(t[x].c[0],t[x].c[1]);
	t[x].tag^=1;
}
void pushdown(int x){
	if(t[x].tag){
		pushrev(t[x].c[0]),pushrev(t[x].c[1]);
		t[x].tag=0;
	}
}

update

用于在Splay操作前将根到x路径上的点的标记全部下放,来保证每个节点的儿子是正确的。

void update(int x){
	if(!isroot(x)) update(t[x].fa);
	pushdown(x);
} 

rotate

和Splay中的操作一致。注意要用isroot判断根。

void rotate(int x){
	int y=t[x].fa,z=t[y].fa,k=get(x);
	if(!isroot(y)) t[z].c[get(y)]=x;//这一句提前,由于isroot判断的特性 
	t[y].c[k]=t[x].c[k^1];
	if(t[x].c[k^1]) t[t[x].c[k^1]].fa=y;
	t[x].c[k^1]=y;
	t[y].fa=x,t[x].fa=z;
	pushup(y),pushup(x);
}

splay

正常操作

void splay(int x){
	update(x);
	int y=t[x].fa;
	while(!isroot(x)){
		if(!isroot(y)){
			rotate(get(y)==get(x)?y:x);
		}
		rotate(x);
		y=t[x].fa;
	}
}

LCT基本操作

access

LCT中最重要/难理解的操作。

令access(x)表示原树中x到根路径上的所有边改成实边,且与这些边相邻的边全改成虚边。我们先看代码。

void access(int x){
	int y=0;
	while(x){
		splay(x);
		t[x].c[1]=y;
		pushup(x);
		y=x,x=t[x].fa;
	}
}

实际只有四步,(x为y虚边上的父亲),把x旋转到当前Splay的根,令x的右子节点为y,更新x的信息,继续上跳寻找下一个当前splay由虚边指向的点。

沿用上面辅助树的图。

原树

辅助树,一种可能的情况

执行一次 access(N) 我们希望它能变成:

即,把原树A到N路径上的边都变成实边,拉成一棵Splay。实现上考虑从下向上更新 Splay。

makeroot

重要性丝毫不亚于access。我们在需要维护路径信息时,一定会出现路径深度无法严格递增的情况,但根据定义,这种路径是不能出现在一棵Splay中的。

这时候我们需要用到makeroot。

makeroot的作用是使指定的点成为原树的根。

考虑如何实现:对makeroot(x),先做access(x),先把x到根打通。

将树用有向图表示出来,给每条边定一个方向,表示从儿子到父亲的方向。不难发现,换根相当于将x到根的路径的所有边反向(画图理解即可)。

又有,Splay维护的是中序遍历得到深度递增的实链,那么将x到原树根的路径翻转。注意一定要splay(x),让x为根,然后再把以x为根的Splay树区间翻转。不这样写会wa的很惨

void pushrev(int x){
	swap(t[x].c[0],t[x].c[1]);
	t[x].tag^=1;
}
void makeroot(int x){
	access(x),splay(x);
	pushrev(x);
}

findroot

注意!findroot找的是原树的根,并非辅助树的根!

先access(x),再splay(x),此时以x为根的Splay就代表根到x的实链。又根据辅助树上Splay的特性,不断走左儿子即可。

找到根节点之后还要splay(x)保证复杂度。(这东西涉及复杂度证明,记住就行)

int findroot(int x){
	access(x),splay(x);
	while(t[x].c[0]){
		pushdown(x);
		x=t[x].c[0];
	}
	splay(x);
	return x;
}

split

split(x,y)就是将x到y的路径拿出来变成一棵Splay。

先makeroot(x),再access(y),如果要求y是根,就splay(y)。

这三个操作能直接把需要的路径拿到y的子树上,从而进行其他操作。

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

就是将x和y之间连边。先让x成为子树的根,再让y成为x的父亲,x成为y的虚儿子即可。

注意题目不保证合法时要先判x-y直接有没有边。

void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x) t[x].fa=y;
}

cut

先做一次makeroot(x),再做access(y)和splay(x)。就能保证若两点之间有连边,则一定是实边,且y一定是x的右儿子且y没有左儿子。把它们双向断开即可,注意更新x信息。

void cut(int x,int y){
	makeroot(x),access(y),splay(x);
	if(t[y].fa==x&&!t[y].c[0]) t[y].fa=t[x].c[1]=0;
	pushup(x);
}

终于结束了,累死我了

拼起来就是

[模板] 动态树(LCT)
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m,a[maxn],tot;
struct node{
	int c[2],fa,sum,val,tag;
}t[maxn];
void newnode(int val){
	tot++;
	t[tot].val=t[tot].sum=val;
}
int get(int x){
	return t[t[x].fa].c[1]==x;
} 
bool isroot(int x){
	return t[t[x].fa].c[0]!=x&&t[t[x].fa].c[1]!=x;
}
void pushup(int x){
	t[x].sum=t[t[x].c[0]].sum^t[t[x].c[1]].sum^t[x].val;
}
void pushrev(int x){
	swap(t[x].c[0],t[x].c[1]);
	t[x].tag^=1;
}
void pushdown(int x){
	if(t[x].tag){
		pushrev(t[x].c[0]),pushrev(t[x].c[1]);
		t[x].tag=0;
	}
}
void update(int x){
	if(!isroot(x)) update(t[x].fa);
	pushdown(x);
} 
void rotate(int x){
	int y=t[x].fa,z=t[y].fa,k=get(x);
	if(!isroot(y)) t[z].c[get(y)]=x; 
	t[y].c[k]=t[x].c[k^1];
	if(t[x].c[k^1]) t[t[x].c[k^1]].fa=y;
	t[x].c[k^1]=y;
	t[y].fa=x,t[x].fa=z;
	pushup(y),pushup(x);
}
void splay(int x){
	update(x);
	int y=t[x].fa;
	while(!isroot(x)){
		if(!isroot(y)){
			rotate(get(y)==get(x)?y:x);
		}
		rotate(x);
		y=t[x].fa;
	}
}
void access(int x){
	int y=0;
	while(x){
		splay(x);
		t[x].c[1]=y;
		pushup(x);
		y=x,x=t[x].fa;
	}
}
void makeroot(int x){
	access(x),splay(x);
	pushrev(x);
}
int findroot(int x){
	access(x),splay(x);
	while(t[x].c[0]){
		pushdown(x);
		x=t[x].c[0];
	}
	splay(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;
}
void cut(int x,int y){
	makeroot(x),access(y),splay(x);
	if(t[y].fa==x&&!t[y].c[0]) t[y].fa=t[x].c[1]=0;
	pushup(x);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		newnode(a[i]);
	}
	while(m--){
		int opt,x,y;
		scanf("%d%d%d",&opt,&x,&y);
		if(opt==0){
			split(x,y);
			printf("%d\n",t[y].sum);
		} 
		else if(opt==1) link(x,y);
		else if(opt==2) cut(x,y);
		else{
			splay(x);//转到根才能不影响前面的节点 
			t[x].val=y;
			pushup(x);
		}
	}
	return 0;
}

例题+手法

LCT维护边权:[POJ3237]树的维护

这题本可以直接用树剖维护,但我们想要练习LCT的维护边权以应对更复杂的情况

注意到我们不能直接边权下放点权,因为LCT的根是会变化的。我们考虑给x-y之间的边设一个字母z,变成x-z-y。把所有的边权的信息都放到z上去做LCT,然后正常维护就可以了。

LCT维护最小生成树:[Wc2006] 水管局长

开始思路在维护无向图的环中绕了很久。结果突然发现人求的是最长时间的最小值。

直接用LCT维护最小生成树就行了。开始直接建最小生成树,然后后面的删边改加边。

每次加边时取x-y的路径,比较当前边和路径上最值边的大小,若当前边更优,则替换掉原树中那条边。

LCT维护连通块:[Codechef MARCH14] GERALD07 加强版

真没想出来,感觉线段树套LCT时间复杂度不对。

还是线段树。

经典结论:森林的连通块个数是点数-边数。问题就转化为[l,r]内可以成功插入的边,再转化为不可成功插入的边。

考虑逐个往后枚举插入,直到插入某个边成环。这时我们就知道,这个环上的边不能共存。此时我们应该删去编号最小的边。思考一下为什么要删最小的边,其实想法类似于前缀线性基,就是这样对于当前r能包含到的l最多(因为这环上的边随便删一个就行,就是至少有一个环上的边小于查询区间l)。

我们记录一下当r到某个数时它的l小于等于某个值时,会贡献+1。二维数点,用主席树解决。

闲话:这用LCT维护图基本上都是转化为生成树去做的。

[THUWC2017] 在美妙的数学王国中畅游

好家伙我就说看不出来怎么维护。

居然是用泰勒展开把这三个式子都转化成多项式去维护,还要求导,我是一个都不会。

展开之后的维护操作都是LCT基本操作了。且考虑到精度限制,我们只需要算到20位就可以了。

权值LCT维护LCT:[bzoj3159] 决战

这题最难绷的是要翻转权值,而不是连着区间的点一起翻转。在LCT中本来写的区间翻转是连着点一起翻,导致每个点对的权值反而没变。实际上是不是分开维护这两个翻转就行?

nonono,两个子树大小不一定一样,不能一一对应。很简单的原因啊,我真是唐了。

用LCT维护LCT。就是开两棵LCT,一个维护权值,一个维护位置。对位置树的每个节点维护一个pos值,表示这点的形态splay对应的的权值splay中的任意一点,就是为了找到一个对应的splay。更新的时候注意任意操作都要两个树一起更新了!!!因为权值树没有位置信息,所以必须和位置树一起更新。

另外,这题给了一定是一条链的性质,因此可以直接用树剖 + FHQ-treap维护,双log。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5;
int n,m,rt,pos[maxn];
struct node{
	int fa,c[2],tag,add,mn,mx,v,siz;
	ll sum;
}t[maxn];
void init(){
	t[0].mn=2e9;
	for(int i=1;i<=n;i++) pos[i]=i+n,pos[i+n]=i;
	for(int i=1;i<=2*n;i++) t[i].siz=1;
}
void pushup(int x){
	t[x].sum=t[t[x].c[0]].sum+t[t[x].c[1]].sum+t[x].v;
	t[x].siz=t[t[x].c[0]].siz+t[t[x].c[1]].siz+1;
	t[x].mn=min({t[t[x].c[0]].mn,t[t[x].c[1]].mn,t[x].v});
	t[x].mx=max({t[t[x].c[0]].mx,t[t[x].c[1]].mx,t[x].v});
}
void pushrev(int x){
	swap(t[x].c[0],t[x].c[1]);
	t[x].tag^=1;
}
void pushadd(int x,int add){
	t[x].sum+=1ll*t[x].siz*add;
	t[x].mx+=add,t[x].mn+=add;
	t[x].add+=add,t[x].v+=add; 
}
void pushdown(int x){
	if(t[x].tag){
		pushrev(t[x].c[0]);
		pushrev(t[x].c[1]);
		t[x].tag=0;
	}
	if(t[x].add){
		if(t[x].c[0]) pushadd(t[x].c[0],t[x].add);
		if(t[x].c[1]) pushadd(t[x].c[1],t[x].add);
		t[x].add=0;
	}
}
bool isroot(int x){
	return t[t[x].fa].c[0]!=x&&t[t[x].fa].c[1]!=x;
}
int get(int x){
	return t[t[x].fa].c[1]==x;
}
void update(int x){
	if(!isroot(x)) update(t[x].fa);
	pushdown(x);
}
void rotate(int x){
	int y=t[x].fa,z=t[y].fa,k=get(x);
	if(!isroot(y)) t[z].c[get(y)]=x;
	else pos[x]=pos[y];
	t[t[x].c[!k]].fa=y;
	t[y].c[k]=t[x].c[!k],t[x].c[!k]=y;
	t[x].fa=z,t[y].fa=x;
	pushup(y),pushup(x);
}
void splay(int x){
	update(x);
	int y=t[x].fa;
	while(!isroot(x)){
		if(!isroot(y)) rotate(get(x)==get(y)?y:x);
		rotate(x);
		y=t[x].fa;
	} 
}
int find(int x,int k){
	pushdown(x);
	if(t[t[x].c[0]].siz+1==k) return x;
	else if(k<=t[t[x].c[0]].siz) return find(t[x].c[0],k);
	else return find(t[x].c[1],k-t[t[x].c[0]].siz-1);
}
void setpos(int x){//就是寻找对应点并把它放到根,相当于带上权值树的splay 
	splay(x);
	splay(pos[x]);
	pos[x]=find(pos[x],t[t[x].c[0]].siz+1);
	splay(pos[x]);
}
void access(int x){//这里一定要更新权值树,因为它已经失去了形态,所以在更新形态时要把权值跟上 
	int y=0;
	while(x){
		setpos(x);
		pos[t[x].c[1]]=t[pos[x]].c[1],t[t[pos[x]].c[1]].fa=0;
		t[x].c[1]=y,t[pos[x]].c[1]=pos[y],t[pos[y]].fa=pos[x];
		pushup(x),pushup(pos[x]);
		y=x,x=t[x].fa;
	}
}
void makeroot(int x){
	access(x),setpos(x);
	pushrev(x),pushrev(pos[x]);
}
int split(int x,int y){
	makeroot(x),access(y),setpos(y);
	return pos[y];
}
void link(int x,int y){
	makeroot(x),setpos(y);
	t[x].fa=y;
}
int main(){
	scanf("%d%d%d",&n,&m,&rt);
	init();
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		link(u,v);
	}
	while(m--){
		string s; int x,y,val;
		cin>>s; scanf("%d%d",&x,&y);
		if(s[2]=='c'){
			scanf("%d",&val);
			pushadd(split(x,y),val);
		} else if(s[2]=='m') printf("%lld\n",t[split(x,y)].sum);
		else if(s[2]=='j') printf("%d\n",t[split(x,y)].mx);
		else if(s[2]=='n') printf("%d\n",t[split(x,y)].mn);
		else pushrev(split(x,y));
	}
	return 0;
} 

本文作者:YYYmoon

本文链接:https://www.cnblogs.com/YYYmoon/p/18685475

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   YYYmoon  阅读(18)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起