Splay进行区间操作的原理就在于,其使用排名来构成区间下标,这样可以适应区间的增删
比如提取出区间 [ l , r ] [l,r] [l,r],我们仅需要将排名为 l − 1 l-1 l−1的节点旋转至根,再将排名为 r + 1 r+1 r+1的节点旋转至根的右儿子,此时根节点的右儿子的左儿子便是整个区间 [ l , r ] [l,r] [l,r],当然在实际应用中有哨兵这个恶心的玩意,于是我们需要对查询的排名进行 + 1 +1 +1
对于区间操作的Splay有一个好的建树方法,也即我们将 a [ 1 ] , a [ n + 2 ] a[1],a[n+2] a[1],a[n+2](a表示原序列)设为哨兵,至于哨兵的值是0/-inf/inf需要根据具体情况而定,然后运用线段树的建树方式,建立出一个Splay,然后每次区间操作的时候都将 l , r l,r l,r加上1
当然,Splay也有懒标记的扩展,但需要注意的是,对于懒标记的下传,一般有两种形式,一种是使用find函数下传,一种是在Splay函数中下传,这里我们使用find下传
对于Splay维护区间操作的核心函数在于两个:spilt and merge
spilt 分裂,merge合并
void splay(int x,int goal){//这里小小偷个懒
goal=f(goal);
while(f(x)!=goal){
int y=f(x),z=f(y);
if(z!=goal){
rc(y)==x^rc(z)==y?rotate(x):rotate(y);
}
rotate(x);
}
if(!goal)rt=x;
}
int find(int x,int k){ // 查询x的子树上第k大 ,递归写法
if(!x) return 0;
pushdown(x);
int s=siz(ch(x,0))+1;
if(s==k) return x;
else if(s>k) return find(ch(x,0),k);
else return find(ch(x,1),k-s);
}
void spilt(int x,int k,int &a,int &b){//将x中前k个分在a上,将其他的分在b上
a=find(x,k);
splay(a,x);
pushdown(a);//注意这里需要pushdown
b=rc(a);
rc(a)=0,f(b)=0;
pushup(a);
}
int merge(int a,int b){//将b合并在a上
int pos=find(a,siz(a));
splay(pos,a);
rc(pos)=b;
f(b)=pos;
pushup(pos);
return pos;
}//注意全部用pos
下面我们来两道例题感受一下Splay的强大,一般考察的区间操作也就这些了
超级备忘录
题目链接
描述
你的朋友达达被邀请参加一个叫做“超级备忘录”的电视节目。
在这个节目中,参与者需要玩一个记忆游戏。
在一开始,主持人会告诉所有参与者一个数列, A 1 , A 2 , … , A n A1,A2,…,An A1,A2,…,An。
接下来,主持人会在数列上做一些操作,操作包括以下几种:
ADD x y D:给子序列 {Ax,…,Ay} 统一加上一个数 D。例如,在 {1,2,3,4,5} 上进行操作ADD 2 4 1 会得到 {1,3,4,5,5}。
REVERSE x y:将子序列 {Ax,…,Ay} 逆序排布。例如,在 {1,2,3,4,5} 上进行操作 REVERSE 2 4 会得到 {1,4,3,2,5}。
REVOLVE x y T:将子序列 {Ax,…,Ay} 轮换 T 次。例如,在 {1,2,3,4,5} 上进行操作 RE