zoukankan      html  css  js  c++  java
  • 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即可。

    题目

    魔法森林

  • 相关阅读:
    记录一个用关于mybatis对象为null的坑
    微信小程序开发初体验--教你开发小程序
    聚合数据全国天气预报api接口
    免费的无次数限制的各类API接口(2)
    外卖订单爬虫(美团,饿了么,百度外卖)
    如何快速开发出一个高质量的APP——创业谈
    调用手机话费充值API的SDK编写思路
    基于Retrotfit2.1+Material Design+ijkplayer开发的一个APP(新闻,gif 动图,视频播放)
    技巧:利用 Workflow 显示附近的免费 Wi-Fi
    MyDiary,《你的名字。》同款日记应用
  • 原文地址:https://www.cnblogs.com/vege-chicken-rainstar/p/12264631.html
Copyright © 2011-2022 走看看