Splay与LCT

Splay与LCT

本文介绍splay与lct的基本原理与应用

需要的前置知识:二叉排序树,树链剖分原理。

小菜鸡为了应付老师作业写的,诸多错误还请指教。

splay

二叉排序树

二叉排序树或者为空,或者具有下面的性质:若它的左子树非空,则左子树所有的权值均小于它;若右子树非空,则右子树所有的权值均大于它;左右子树均为二叉排序树。

这样这棵树的中序遍历就是升序的,并且我们可以通过维护树上节点的size来动态地支持插入,删除,查询第k大,查询值的排名,找前继与后继等操作。很容易发现,这些操作的复杂度与树的高度相关,最优情况下它们都是log级别。

不过我们很容易构造出数据来使二叉搜索树退化成链,复杂度瞬间爆炸。

于是我们希望在保证二叉排序树性质的同时,改变树的形态,让树变矮,来保证复杂度,这样的数据结构有很多种,统称为平衡树。

splay

splay是算法竞赛里应用较广泛的一类平衡树。它通过旋转与伸展来保证树高。

旋转

定义:把左儿子旋至父亲的位置为右旋,把右儿子旋至父亲位置为左旋。

如上图将x右旋,分为三步:x的祖父变成x的父亲,y的左儿子变成x的右儿子,x的右儿子变成y. 这样中序遍历结果不变,我们改变了树的形态,还维持了二叉排序树的性质。

	void rotate(int x){
		int y=spl[x].fa,z=spl[y].fa;
		if(!y) return;
		int k = spl[y].son[1]==x;				//判断左儿子还是右儿子
		spl[x].fa=z;
		if(z) spl[z].son[spl[z].son[1]==y]=x;
		spl[y].son[k]=spl[x].son[k^1];
		if(spl[x].son[k^1]) spl[spl[x].son[k^1]].fa=y;
		spl[x].son[k^1]=y;
		spl[y].fa=x;
		update(y); update(x);		//y与x所管理的子树发生改变
	}

伸展

研究表明,倘若我们每次访问到(插入,查找,等等)一个节点时,都将其旋转至根节点,就能很大程度维护树的平衡。 但是伸展操作添加了一些东西:

令y为x的父亲。1:y是根,则直接将x旋转一次;2:y非根且x与y同为各自父亲的左儿子或同为右儿子,则先将y旋转一次,再旋转x;3:其他情况正常旋转。

研究表明,这样可以使x到根路径上的点深度大致减一半,并且使得所有操作均摊复杂度为log级别。

下面是将x旋转至想要的节点的代码:

	void splay(int x,int goal){
		int tmp=spl[goal].fa;
		for(int f;(f=spl[x].fa)!=tmp;rotate(x)){
			if(spl[f].fa!=tmp) rotate((x==spl[f].son[1])^(f==spl[spl[f].fa].son[1])?x:f);
		}
		if(!tmp) root=x;
	}

find

在平衡树里找到x并将其伸展至根。这样的话什么找val的排名,前驱后驱都很方便了。

	bool find(int valu){
		int rt=root;
		while(spl[rt].son[spl[rt].val<valu] && spl[rt].val!=valu) 
			rt=spl[rt].son[spl[rt].val<valu];
		splay(rt,root);
		if(spl[rt].val==valu) return true;
		else return false;
	}

其他的插入,删除等操作都和普通二叉排序树类似,区别在于每次插入一个点立即将其splay至根。

splay区间操作

splay可以说是区间操作的大杀器!比线段树还有用(不止一点半点)。考虑下面的问题:

维护一个数列,支持以下操作:1. 在数列第pos位插入一段给定的序列;2. 将当前序列第L位到第R位翻转;3.单点修改,区间修改。

这种问题在算法竞赛里基本只能用splay做。对于原序列,我们用每个值在序列里的位置作为平衡树上节点的权值。

操作1:找到平衡树上排名第pos的点x与第pos+1的点y,将x伸展至根,将y伸展至根的右儿子,根据平衡树性质,此时y的左子树为空,将给定序列建平衡树,此树接到y的左子树即可。

操作2:找到平衡树上排名第L-1的点x与第R+1的点y,同样将x伸展至根,y伸展至根的右儿子,那么y的左子树就是L到R的区间,对左子树的根打上翻转的懒标记。

操作3:单点修改视同区间修改,仿照操作2打懒标记。

	int split(int x,int y){
		splay(x,root);
		splay(y,spl[root].son[1]);
		return y;
	}

题目

总统选举

动态区间第k大

LCT

我们可以通过splay这个强大的数据结构来动态维护一个森林。

我们知道,对于一棵固定的树,我们可以用重链剖分(比较简单,没有学过的话参考洛谷日报:重链剖分)加线段树来支持其任意路径的权值修改与查询。

但是假如这颗树结构不固定,是动态修改的,那么线段树就不管用了。

也就是说对平面上的点我们要实现以下操作:1.若x,y不连通,连接x,y;2.若x,y有边,断开这条边;3.若x,y联通,修改路径上点的权值;4.若x,y联通,查询路径上点的权值和。

参照重链剖分的思想,我们通过实链剖分,用更灵活的splay来维护每条链,这就是link-cut-tree.

实链剖分

参照重链剖分,我们有以下定义:

实儿子:一个节点若有儿子,可指定其任意一个儿子为实儿子,那么其它的儿子都是虚儿子
实边:连向实儿子的边
虚边:连向虚儿子的边
实链:由一个虚儿子,不断经过实边直到不能连为止,所得的链

区别在于,这里的实和虚是可以动态转换的,而不是固定的。

LCT使用splay这样来维护一棵树:

1.每条实链存在且仅存在于一棵splay中,并且对其中序遍历所得的节点序列在原树中的深度严格递增
2.每棵splay的根节点通过虚边连向另一棵splay或者0,虚边通过连向父亲的单向边来表示。

确实有点绕,结合图来看一下:

图中粗边为实边。那么树被剖分成三条实链:(1,2,3),(4,5),(6).

这三条实链根据各自节点深度关系形成三棵splay,每棵splay的根向另一棵splay连一条虚边(右图中的单向边),可以理解为虚边是两条链之间的边,而非点与点之间的边。

关于虚边的单向边存储,比如要x向y连一条虚边,那么spl[x].fa=y; 但是y的左右儿子都不是x.

下面介绍LCT基本操作:

access

LCT核心操作,作用是将节点N与原树根节点之间的链连成一条实链,那么有些实边需要变成虚边,有些虚边需要变成实边,这就体现了LCT的虚实变换。

N与根之间可能隔着许多splay,我们需要从N开始不断向上,把N变成所在splay的根,改变N与其父亲之间边的虚实关系,合并splay,由于N上面的点深度小于N,所以N变成父亲的右儿子,同时父亲与原来右儿子之间的边自然变成虚边。

直接看代码:

void access(int x){
	for(int y=0;x;){     //y是上一棵树的根节点,初始为0
		splay(x);          //将当前的点伸展到所属的splay的根节点
        spl[x].son[1]=y;   //改变虚实
        pushup(x);			
        y=x;
        x=spl[x].fa;
    }	
}

关于LCT的rotate,splay操作与一般伸展树略有不同,下面会将。

makeroot

LCT重要操作,将某个点N变成原树的根,可以理解为对原树的换根,改变原树形态。

首先access(N),这样换根只对N到根这条链上的点有影响。在这条链的splay里,显然N的深度最大,根的深度最小,换根(实际上相当于倒转这条链)后则变成N的深度最小,根的深度最大,于是我们只需要splay(N),然后给N打上区间翻转的懒标记。

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

findroot

LCT重要操作,找到原树的根。

也很简单,access(N),然后找到splay里排名为1的点(深度最小)。

int findroot(int x){
	access(x);
	splay(x);
	while(spl[x].son[0]) pushdown(x), x=spl[x].son[0];
	splay(x);			//保证树的平衡性
	return x;
}

split

将原树中两个点之间的路径拉成一条splay

看代码就懂了

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

有了上面的东西,就可以实现题目所要求的操作啦!

连接两个点:

void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x) spl[x].fa=y;  //不连通,则连一条虚边
}

断开一条边:

void cut(int x,int y){
	makeroot(x);
	if(findroot(y)==x&&spl[y].fa==x&&!spl[y].son[0])  //关于第三个,代表x,y间没有其它点
		spl[x].son[1]=spl[y].fa=0,pushup(x);
}

关于LCT中的rotate与splay,多了一个判断根的语句,意会意会

bool nroot(int x){
	return spl[spl[x].fa].son[0]==x||spl[spl[x].fa].son[1]==x;  //判断一个点不是所在splay的根
}
void rotate(int x){
	int y=spl[x].fa,z=spl[y].fa;
	if(!nroot(x)) return;
	int k=(spl[y].son[1]==x);
	if(nroot(y)) spl[z].son[spl[z].son[1]==y]=x;	//注意
	spl[x].fa=z;
	spl[y].son[k]=spl[x].son[k^1];
	if(spl[x].son[k^1]) spl[spl[x].son[k^1]].fa=y;
	spl[x].son[k^1]=y;
	spl[y].fa=x;
	pushup(y); pushup(x);
}
int stk[maxn];
void splay(int x){
	int y=x,z=0;
	stk[++z]=y;
	while(nroot(y)) stk[++z]=y=spl[y].fa;		//用栈从上往下释放懒标记
	while(z) pushdown(stk[z--]);
	while(nroot(x)){
		y=spl[x].fa,z=spl[y].fa;
		if(nroot(y))
			rotate((spl[y].son[1]==x)^(spl[z].son[1]==y)?x:y);
		rotate(x);
	}
}

查询

查询的话,把路径拉成一条splay即可。

题目

魔法森林

posted @ 2020-02-05 17:32  rain_star  阅读(154)  评论(3编辑  收藏  举报