这个splay也是个很有趣的数据结构,它是为了平衡权值二叉树而提出的,却最终不是以权值二叉平衡树的形式被广泛使用,而是去维护序列的区间修改(某些线段树实现不了的区间修改,如区间翻转等),很多萌新在初学splay时都看的是平衡权值二叉树形式的,所以突然接触区间翻转等问题会有困难。这里我就不讲splay的权值平衡树的形式了(网上资料很多),而是主要讲一讲怎么去理解splay去维护区间(几乎没有人解释的很清楚)。
在学习splay维护区间时,一定要摒弃权值树的思维定式。这时的splay的每一个key键值对应原序列的a[i],但是注意这个key在splay中不是一个二叉查找树,即不一定满足key的左孩子都小于key,右孩子都大于key,那我们根据什么去建一个splay呢?我们根据原序列的下标去建一个尽量平衡的二叉树(每次取中点作根),那么我们发现无论怎么旋转,一个key[u]下标对应size[left[u]]+1总是对应这个key[u]在原序列中的下标,例如我们需要查找a[k](select(root,k)),那么我们从根做起,若size[left[root]]+1>=k,说明我们需要的a[k]在左子树中,递归select(left[root],k),若size[left[root]]+1
一定要注意,我们没有以权值的形式去查找a[k],而是以排名的形式查找k。
讲了这么多都是空话,我们解决一道实际问题吧:洛谷P3391,文艺二叉树,区间翻转的模板题。
既然我们可以用splay将一个下标k翻转到根,那么当我们需要翻转区间[l,r]时,我们可以先将下标l-1翻转到根,然后将下标r+1翻转到根的右子树,那么这时根的右子树的左子树就是我们需要的下标[l,r]的区间,打上lzay标记。然后在每次select找下标时,不断的把标记下传,同时把左右子树交换一下,这时不就相当于交换了下标嘛?
再好好理解一下,splay的这个特性使其有了与Treap和SBT竞争的余地。
下面就是我在洛谷写的题解了:
注意,这时的splay不再是一个二叉权值查找树,这时给每个节点维护一个size值表示子树中节点的个数。
我们先把原序列按照下标建成一个完美的平衡二叉树,那么这时对于一个u,它的size[left[u]]+1对应的在原序列中的下标,
这时我们发现不论怎么旋转,每个u的size[left[u]]+1是不会变的,
因此,当我们需要维护原序列的[l,r]序列的翻转时,我们可以把对应的size[left[u]]+1=l-1的先旋转到根节点,然后把对应size[left[u]]+1=r+1的旋转到根节点的右子树,那么此时这个右子树的左子树就是l<=size[left[u]]+1<=r,然后我们给这个节点打上lazy标记,在每次查找size[left[u]]=k时若u上有标记,将u的左右子树交换,并下传标记,在输出结果时也下传标记就行了。
一定要理解:splay不再是一个二叉权值查找树,它维护的key是原序列对应下标的值,它的中序遍历就对应了原序列,它根据size进行查找原序列的下标,此时我们要维护区间时就可以类似线段树的延迟操作打上lazy标记。然后下传到左右子树就行了。
这个偷梁换柱很巧妙,因为splay独特的伸展操作使它可以维护一个区间。若仅仅是需要一个平衡树,那么splay是没有使用的必要的,因为Treap也支持合并分裂,而SBT可以最快最稳定的完成所有平衡二叉树的操作。
参考代码:
#include
#include
using namespace std;
const int N=101000;
int ch[N][2];
int size[N],rev[N],fa[N];
int n,m,rt=0;
int read(){
int x=0,f=1;char ch=getchar();
while (ch<'0' || ch>'9'){if (ch=='-')f=-1;ch=getchar();}
while ('0'<=ch && ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return x*f;
}
void pushup(int x){
size[x]=size[ch[x][0]]+size[ch[x][1]]+1;
}
void pushdown(int x){
if (rev[x]){
int &lc=ch[x][0],&rc=ch[x][1];
swap(lc,rc);
rev[lc]^=1;rev[rc]^=1;rev[x]=0;
}
}
void build(int l,int r,int &rt){
if (l>r) return;
int mid=(l+r)>>1;
if (mid