一.几个重要概念
1.伸展树属于一种平衡树,也是一棵普通的二叉排序树。
2.伸展树的核心在于它的伸展(splay)操作,对于每一次的伸展操作(把某个节点放到目标节点的下面),都有可能改变树中每个节点的分布,从而改变整个树的形状。
3.伸展树对于树的平横性没有要求,与平衡树不同,任意两个节点都可以有任意的深度差,不需要记录平衡树的冗余信息。
4.伸展树每次搜索的复杂度平摊下来都是log(n),如果遇到插入的数每次都是两个极端的情况,此时伸展树退化为链状,复杂度最坏。
5.伸展树的旋转操作
伸展树中的旋转操作不同于平衡树,一般对于这个操作最基本的方法就是一层一层的向上旋转,无法改变树的形态,而伸展树中不需要
控制树的形态,从而有新的方法来进行旋转。
在伸展树的旋转操作一共可以分为三类(按形状分),每类都有镜像。
1 )单旋转:当前节点的父节点即为目标节点,那么直接左旋或者右旋即可。
goal x
/ -> \
x goal
2)一字型:顾名思义树枝的形状呈现为一字型,如图
z x
/ y \
y -> / \ -> y
/ x z \
x z
此时先对y进行旋转,再对x进行旋转
3)之字形:顾名思义树枝的形状呈现为之字形,如图
z z
/ / x
y -> x -> / \
\ / y z
x y
此时先对x旋转,之后再对x进行一次旋转
以上的旋转左旋还是右旋辨别有一个小窍门,对于x来说,如果是y的左节点,那么右旋,如果是y的右节点,那么左旋,
一字型的两次旋转方向相同,之字形相反。
-------------------------------------以上是基础必备知识------------------------------------------
那么我们如何像线段树一样运用伸展树对数列的区间经行操作呢?
先来看一道题目
Description
给出了一个序列,你需要处理如下两种询问。
"C a b c"表示给[a, b]区间中的值全部增加c (-10000≤ c ≤ 10000)。
"Q a b" 询问[a, b]区间中所有值的和。
Input
第一行包含两个整数N,Q。1≤ N,Q ≤ 100000.
第二行包含n个整数,表示初始的序列A (-1000000000≤ Ai ≤1000000000)。
接下来Q行询问,格式如题目描述。
Output
对于每一个Q开头的询问,你需要输出相应的答案,每个答案一行。
Sample Input
10 5 1 2 3 4 5 6 7 8 9 10 Q 4 4 Q 1 10 Q 2 4 C 3 6 3 Q 2 4
Sample Output
4 55 9 15
题意很明确,就是给出更新操作和查询操作让我们来执行,用线段树来写很方便,效率很高,但是如果用伸展树该如何解决。
首先我们知道伸展树的中序序列就是我们要维护的区间,那么我们假设现在要维护的区间为[a,b],那么如果a-1号节点的右儿子刚好是b+1,那么此时我们的区间就刚好是
b+1号节点的左儿子?如图
为了防止b+1不能直接转移到a-1的右儿子上,我们直接把a-1放到根节点上,继而b+1号节点就能顺理成章的转移到a-1的右节点上
这样以来区间就能确定了,但是由于a-1和b+1都有有可能越界,我们需要另外虚设两个节点防止越界,上代码:
#define keytree ch[ch[root][1]][0]
void newnode(int &x,int p,int v)
{
x=++top;//为每个节点分配编号
ch[x][0]=ch[x][1]=0;//每个节点末端初始化为终端节点
pre[x]=p;
val[x]=v;
add[x]=0;
sum[x]=v;
siz[x]=1;
}
void init(int n)
{
for(int i=0; i
两个虚设的节点在整个数列的两端维护整个数列
对于0-n-1个结点来说,为了从一开始就尽可能减少复杂度,我们从中间节点开始建树,如代码:
void build(int &x,int l,int r,int p)
{
int mid=(l+r)>>1;
newnode(x,p,tmp[mid]);
if(lmid) build(ch[x][1],mid+1,r,x);
pushup(x);
}
那么样例中的树建好后应该是这样
图中的数字代表的是节点的编号,而不是原数列中的下标,不要被迷惑了。
每次的区间就被安排在了keytree那个位置
对于图中的的每一个节点来说,他在序列中对应的编号就是它的左树节点个数+1,在找第原序列中的第几号时按照的就是这个规律。
题目源代码:
#include
#include
#include
#include
using namespace std;
#define maxn 100020
#define keytree ch[ch[root][1]][0] ///keytree代表splay之后的区间节点
int ch[maxn][2],pre[maxn],tmp[maxn],val[maxn],siz[maxn],add[maxn];
///ch用来存储节点的左孩子右孩子,add为延迟标记,siz代表子树中有多少个节点
long long sum[maxn];
int top,root;
/*debug部分
void travel(int r)
{
if(r)
{
travel(ch[r][0]);
printf("node: %2d l: %2d r: %2d pre: %2d val: %2d siz: %2d add: %2d sum: %2d\n",r,ch[r][0],ch[r][1],pre[r],val[r],siz[r],add[r],sum[r]);
travel(ch[r][1]);
}
}
void debug()
{
printf("root= %d\n",root);
travel(root);
}
*/
///newnode部分主要功能是为每个节点分配一个编号和初始化该节点信息,注意这里的引用
void newnode(int &x,int p,int v)
{
x=++top;
ch[x][0]=ch[x][1]=0;
pre[x]=p;
val[x]=v;
add[x]=0;
sum[x]=v;
siz[x]=1;
}
///和线段树一样的操作
void pushup(int x)
{
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
sum[x]=sum[ch[x][0]]+sum[ch[x][1]]+val[x];
}
void pushdown(int x)
{
if(add[x])
{
add[ch[x][0]]+=add[x];
add[ch[x][1]]+=add[x];
val[ch[x][0]]+=add[x];
val[ch[x][1]]+=add[x];
sum[ch[x][0]]+=(long long)siz[ch[x][0]]*add[x];
sum[ch[x][1]]+=(long long)siz[ch[x][1]]*add[x];
add[x]=0;
}
}
///由数列的中间开始建树
void build(int &x,int l,int r,int p)
{
int mid=(l+r)>>1;
newnode(x,p,tmp[mid]);
if(lmid) build(ch[x][1],mid+1,r,x);
pushup(x);
}
///初始化终端节点,申请两个虚拟节点,建树
void init(int n)
{
for(int i=0; i