前言
splay是伸展树,是平衡树中的一种。
它主要就是利用神奇的旋转操作来让一颗二叉查找树维护东东。
很早就学过它的旋转,如今早已忘光。
于是接下来就权当新的学习。
我好蔡啊!
引入
首先我们知道这个二叉查找树是一个十分美妙的树。
但是光有这么一颗树是没有用的。
但是一旦用上splay或spaly,可以维护修改、查询等简单操作。更甚者加入点、删除点、区间翻转这样的高级操作。
而这一切都是从一个叫做旋转的东东来实现的。
更新操作
就是把splay中一个点在旋转后更新其中的值。
很线段树很像,不写了。叫做update
万恶之源旋转操作
经典配图。
首先我们利用这个图可以发现。
我们把2右旋时,就是把它的父亲当做自己右儿子,把自己原来的右儿子变成1的左儿子
左旋同理。
于是可以得到一个比较清真的旋转方式:
procedure left_rotate(x:longint);
var
y,z:longint;
begin
y:=father[x];
z:=father[y];
right[y]:=left[x];
if (left[x]<=tot) and (left[x]<>0) then
begin
father[left[x]]:=y;
end;
father[x]:=z;
if z<>0 then
begin
if left[z]=y then left[z]:=x
else right[z]:=x;
end;
left[x]:=y;
father[y]:=x;
end;
procedure right_rotate(x:longint);
var
y,z:longint;
begin
y:=father[x];
z:=father[y];
left[y]:=right[x];
if (right[x]<=tot) and (right[x]<>0) then
begin
father[right[x]]:=y;
end;
father[x]:=z;
if z<>0 then
begin
if left[z]=y then left[z]:=x
else right[z]:=x;
end;
right[x]:=y;
father[y]:=x;
end;
既然我们知道了这个东东后。
我们可以就可以直接一波旋转把任意的点旋转到它的祖先的任意位置。
所以说,如果x为左儿子,则右旋,反之则左旋。
转转不已,遂反父亲逆上矣。
当然,要注意如果当前点、父亲、爷爷(我)在同一条线上(都是左儿子或右儿子)时,要先选父亲。
这个似乎是要满足一个叫做势能分析的东东。
下面这个程序是把x旋转到a的操作(核心操作)
procedure splay(x,a:longint);
var
y,z:longint;
begin
while father[x]<>a do
begin
y:=father[x];
z:=father[y];
if z<>a then
begin
if (left[z]=y) and (left[y]=x) then
begin
right_rotate(y);update(z);update(y);
right_rotate(x);update(y);update(x);
end
else
if (right[z]=y) and (right[y]=x) then
begin
left_rotate(y);update(z);update(y);
left_rotate(x);update(y);update(x);
end
else
if (right[z]=y) and (left[y]=x) then
begin
right_rotate(x);update(y);update(x);
left_rotate(x);update(z);update(x);
end
else
if (left[z]=y) and (right[y]=x) then
begin
left_rotate(x);update(y);update(x);
right_rotate(x);update(z);update(x);
end;
end
else
begin
if left[y]=x then
begin
right_rotate(x);update(y);update(x);
end
else
if right[y]=x then
begin
left_rotate(x);update(y);update(x);
end;
end;
end;
if a=0 then root:=x;
update(x);
end;
原谅我打得丑。
当然,在旋转的同时,要维护一下size的大小。
至此,最基本的旋转操作介绍完毕。
建图
两种方法——
1、直接从小到大建一条链。
为什么呢?
因为我们在建完后随便怎么旋转多次,都可以得到一个比较平衡的状态。
均摊是log级别的。
2、把二叉搜索树建出来。
这个不解释了。
function build(l,r:longint):longint;
var
m:longint;
begin
m:=(l+r) div 2;
{在这里附初值}
build:=m;
if l<m then
begin
left[m]:=build(l,m-1);
father[left[m]]:=m;
end;
if m<r then
begin
right[m]:=build(m+1,r);
father[right[m]]:=m;
end;
update(m);
end;
(注意,接下来的所有操作完后,都要把目标splay到根节点。具体为什么,因为势能分析。)
寻找操作
方法:
和二叉搜索树的方法很像,每次往树的两边走,若是插入的数大于左儿子,则往右走,反之往左走。
然后把找到的点splay到根。
function find(x:longint):longint;
var
t:longint;
begin
t:=root;
while true do
begin
if size[left[t]]+1=x then
begin
exit(t);
end
else
if size[left[t]]+1>x then
begin
t:=left[t];
end
else
if size[left[t]]+1<x then
begin
x:=x-size[left[t]]-1;
t:=right[t];
end;
end;
end;
区间操作
区间操作是指把l到r区间内的数弄到一个独立的子树的操作。
如何?
首先把lsplay到根,然后把rsplay到l的右儿子。
画个图可以发现,l+1到r-1的区间就在r的左子树中。
procedure get(l,r:longint);
var
i:longint;
begin
i:=find(l);splay(i,0);
i:=find(r);splay(i,root);
end;
区间修改(插入、删除)
这个其实我们有了“区间操作”这个神奇的东东后,很容易实现。
把区间头和尾分别旋转一下,需要搞的区间就在尾的左子树。
挂标记、加入数字、删除数字三个愿望一次满足。
下面的程序分别是插入、删除、修改(一个数字)
procedure insert(x,u:longint);
var
i:longint;
begin
inc(tot);
tree[tot].val:=u;
update(tot);
get(x,x+1);
i:=right[root];
left[i]:=tot;
father[tot]:=i;
update(i);
update(root);
end;
procedure delete(x:longint);
var
i:longint;
begin
get(x,x+2);
i:=right[root];
left[i]:=0;
update(i);
update(root);
end;
procedure change(x,u:longint);
var
i:longint;
begin
get(x,x+2);
i:=left[right[root]];
{此处修改}
update(i);
update(right[root]);
update(root);
end;
询问
同上,随意弄区间,然后询问即可。
区间翻转
这个操作其实也是把一个区间弄在一颗子树。
之后打个标记表示该子树需要旋转。
如何标记下传?
把当前节点的左右儿子互换,下传标记即可。
代码就不写了吧。
求前驱与后继
首先,前驱后继是指什么呢?
前驱:小于x,且最大的数
后继:大于x,且最小的数
这个其实有一个套路。
把x插入到splay中,然后删除掉即可。
找到?利用二叉搜索树的性质,与上面寻找同理。
这些操作大概就很完备了。
例题
jzoj2744. 【SPOJ】GSS6
jzoj3599. 【CQOI2014】排序机械臂
bzoj1588: [HNOI2002]营业额统计
欠的旧债
spaly是什么?
其实就是假的splay。
我多喜欢吧spaly看做是单旋的splay,单旋其实就是指旋转不看爷爷,只看父亲。
大多数情况下,spaly比双旋(看爷爷)的splay快,但是容易被卡。