伸展树Splay【非指针版】

·伸展树有以下基本操作(基于一道强大模板题:codevs维护队列)

image

image

image

a[]读入的数组;id[]表示当前数组中的元素在树中节点的临时标号;fa[]当前节点的父节点的编号;c[][]类似于Trie,就是一个邻接表,存储左右儿子编号;sum[]区间和;size[]当前根节点所在区间的大小;v[]节点权值;mx[]当前区间连续和最大值;rx[]当前区间右端点连续和最大值lx[]当前区间左端点连续和最大值;rev[]区间反转的LAZY操作;tag[]区间整体赋值修改的LAZY操作;

①“旋学”:旋转操作

使整个树结构保持平衡,使得查询、遍历复杂度始终很优秀(比如说,一棵树变成链后,就变成O(n)复杂度,让人感到遗憾——但是伸展树几乎每一个操作都要进行旋转操作,这使得上述不幸情况不会出现)

有了左旋必有右旋,两两组合成了双旋,它们构成了伸展树的旋转操作。

image

 伸展树许多操作依赖于SPLAY函数,即在一个while()中不断进行双旋。

SPLAY函数依赖于单旋函数:ROTATE(now_node,ch[][])

 image

由于使用了“&”,使得旋转后当前根节点的父亲的其中一个儿子指针可以自然地指向改变后的根节点(x);整个程序体现两个特点:

(1)改变调整亲子关系(2)分类讨论:(如图,以右旋为例)

image

·我们需要反复记忆的是:旋转操作的目的就是将某一节点旋转到根节点(在区间操作时,也可以旋转至根节点的右儿子处)。

这样看来,SPLAY注定也是承袭上面ROTATE函数的特点。

image

(如果有些细节不清晰,可参见下文部分解释和完整代码)

②各类操作的基础函数

·接下来是区间操作,像区间平移,区间插入,区间删除等这样的操作,使得伸展树在不仅具备线段树所有的功能,还可高效完成一些其他的任务。

·谈及区间操作,我们有:单点修改-区间查询和区间修改-区间查询。对于后者,LAZY操作是必不可少的,所以伸展树的代码中会有和线段树的相似之处:PUSHUP函数与PUSHDOWN函数(但大多时候PUSHUP函数由于简洁,便直接写在其它函数里面了)。

现在可以从考虑这样两个关键问题入手【伸展树主要是用什么操作完成了所有的区间操作?】【PUSHDOWN函数是在哪些时候调用?】

·第一个问题的解答是:

·首先要了解伸展树的本质——平衡树。说明左右两边节点之间存在大小关系等,当我们将要进行操作的数组的下标作为建树关键字的时候,此时的伸展树才有了维护区间的作用。如图:

image

·因此:由于2,9号节点不可能每次恰好都在我们想要的位置,所以我们要做的是通过某种操作,在保证树的有序性的前提下,将所求区间的一个端点移动到整棵树的根节点上,另一个端点移动到根节点的右(左)儿子上。这样的话,所求的区间就是根节点的右(左)儿子节点的左(右)子树了。

·把这种方法称为【收口袋】:找到区间左右端点,通过旋转操作,将其移至根节点和根节点的一个儿子上(毫无疑问,这巧妙的利用了SPLAY操作不会改变平衡树性质的特点,让我们要处理的区间被“夹”在两个边界节点之间)。

·在程序中,用SPLIT函数(分离函数)来完成特定区间的收口袋操作。

若设该区间的长度为tot,则有:

image

 

这里就自然引出了FIND函数,用于寻找当前权值的点在树中的编号(注意此编号不同于数组下标,再加之后来的区间删减操作,这两者是无法直接关联的,这里是一个美妙的易错点)FIND函数如下:(利用区间长度size来查询)

image

·有趣的是,FIND函数在树关键字为数值大小时,可以用来求第K大数的具体数值,许多代码中将其改称为Kth_Find函数。

·注意到一个有关第二个问题的细节:FIND函数中出现了PUSHDOWN()。

FIND只是一个查询位置的函数,SIZE[]的值在建树的时候是定下来了的,一般情况下不会改变,所以FIND函数的结果不会受到LAZY操作的影响。为什么在这里要写上一个PUSHDOWN函数?这有助于我们更加深刻的理解该函数的作用——它的使用前提是:①在向下搜索中,需要使用节点信息(比如sum等),那么必须调用PUSHDOWN函数。②在伸展树中,由于FIND函数用于返回某一节点的编号,随之而来的很可能是旋转操作(【收口袋】),我们画图便可明白:在一个节点x多次旋转到达root的过程中,路径上的点的亲子关系,以及随之而来的一系列信息来源(来自左右儿子的信息)都会发生改变,并且除这些点的其他点不会受到任何影响。所以,这些均可作为在哪里调用PUSHDOWN函数的依据。(图为一种PUSHDOWN举例)

image

·以上面几个操作为基础,引出几个伸展树的区间操作。

③区间删除操作

【将原数列[L,R]删除(后面自动补齐)】

思路十分清晰,利用SPLIT函数【收口袋】,将要删除的序列分离成一棵完整的子树,然后将这棵树的与其根节点的关系断掉。为了节省空间,常常用回收函数REC()将删掉的所有节点放入一个队列中,在将来建树的时候(如果有区间插入操作,那么还会多次建树)利用这些空余的空间。(如图)

image

image

· 通过接下来的区间插入操作,可以进一步理解REC函数的作用。

④区间插入操作

【将一个新数列插入到原数列k号元素的后面】

基本步骤是将插入的数列建立一棵新树即BUILD函数,然后用FIND函数找到原数列中k,k+1的位置,最后旋转收口袋,把新树放入口袋中就可以了。

 image

从第4行可以看出,新加入的元素先把以前删除余下的空间给它,用完后在开新的空间给它。注意理解的是,BUILD函数建树过程中,由于多次建树,所以除了判断左右儿子用数组下标x外,其余信息存储都依靠id[x]完成。

image

(l==r下面包含的部分是本题的处理,可以略过)

·需要再次强调的是,只要一个点的儿子们的信息发生了改变,那么要立刻进行UPDATE(PUSHUP)操作。

⑤区间修改操作

【将某一区间全部变成一个数k】

与线段树差不多:

image

区间反转操作

【将[L,R]内的数字翻转】

基本思路是【收口袋】后找到该区间的根节点,然后交换两棵子树位置,注意使用LAZY操作(代码中是REV[]数组保存)

 image

tag[]是区间修改的LAZY数组,所以当tag[]不为零时,说明这一段区间会被改成相同的数,那么就不需要翻转了。

⑦区间交换操作(区间循环平移操作)

【本质是交换两个区间的位置,但后者需要一些处理,它的意思是将区间[L,R]中的数向某一固定方向循环平移t次】

举例:[1,2,3,4,5],t=2,那么向右循环平移,则结果为:[4,5,1,2,3]。所以发现这就是一个区间交换操作,断点在R-t+1处。由于t可能会很大,所以我们需要mod处理。思路是先将后面的区间【收口袋】,然后找到前面区间的左端点,进行一个区间插入操作(但无需建树,因为本来就存在这棵树)。

image

⑧寻找前驱后继操作

【常用来求数据波动大小等问题】

这可以算是比较简单的了。

image

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

不是每一句话都有意义,不是每一场梦都有结局......不是每次祈祷都能解脱,不是每次放逐都能救赎。

————汪峰《再见蒲公英》

posted @ 2017-03-28 21:47  大米饼  阅读(805)  评论(2编辑  收藏  举报