登峰造极之树——Splay伸展树

这个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]]+1select(right[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 (mid0]=mid;else ch[rt][1]=mid;
    fa[mid]=rt;size[mid]=1;
    if (l==r)return;
    build(l,mid-1,mid);build(mid+1,r,mid);
    pushup(mid);
}
void rotate(int x,int &k){
    int y=fa[x],z=fa[y],d=ch[y][1]==x;
    if (y==k)k=x;
    else ch[z][ch[z][1]==y]=x;
    fa[ch[x][d^1]]=y;fa[y]=x;fa[x]=z;
    ch[y][d]=ch[x][d^1];ch[x][d^1]=y;
    pushup(x);pushup(y);
}
void splay(int x,int &k){
    while (x!=k){
        int y=fa[x],z=fa[y];
        if (y!=k){
            if ((ch[z][0]==y)^(ch[y][0]==x))rotate(x,k);
                else rotate(y,k);
        }
        rotate(x,k);
    }
}
int select(int &rt,int k){
    pushdown(rt);
    int sum=size[ch[rt][0]]+1;
    if (sum==k)return rt;
    if (sum>k)return select(ch[rt][0],k);
        else return select(ch[rt][1],k-sum);
}
void rever(int L,int R){
    splay(L,rt);splay(R,ch[L][1]);
    rev[ch[R][0]]^=1;
}
void print(int rt){
    pushdown(rt);
    if (ch[rt][0])print(ch[rt][0]);
    if (2<=rt && rt<=n+1)printf("%d ",rt-1);
    if (ch[rt][1])print(ch[rt][1]);
}
int main(){
    n=read(),m=read();
    rt=(3+n)>>1;build(1,n+2,rt);//将询问区间[l,r]转换成[l+1,r+1],那么我们翻转l-1,r+1时就不需要特判了。
    for (int i=1;i<=m;i++){
        int L=read(),R=read();
        L=select(rt,L);R=select(rt,R+2);
        rever(L,R);
    }
    print(rt);
    return 0;
}


你可能感兴趣的:(常用算法讲解)