返回顶部

平衡树(Splay)

参考自:OI Wiki

伸展树

定义

Splay 树, 或 伸展树,是一种平衡二叉查找树,它通过 Splay/伸展操作 不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,能够在均摊  时间内完成插入,查找和删除操作,并且保持平衡而不至于退化为链。

Splay 树由 Daniel Sleator 和 Robert Tarjan 于 1985 年发明。

结构

二叉查找树的性质

首先肯定是一棵二叉树!

能够在这棵树上查找某个值的性质:左子树任意节点的值  根节点的值  右子树任意节点的值。

节点维护信息

roottotfa[i]lc[i]/rc[i]val[i]cnt[i]si[i]
根节点编号 节点个数 父亲 左右儿子编号 节点权值 权值出现次数 子树大小

操作

基本操作

  • siz(x):在改变节点位置后,将节点x的size更新。
  • wh(x):判断节点  是父亲节点的左儿子还是右儿子。
复制代码
inline bool wh(int x)//判断是否为右子树; (具体用法见操作splay)
{
    return rc[fa[x]]==x;
}

inline void siz(int x)//计算子树大小size 
{
    si[x]=si[lc[x]]+si[rc[x]]+cnt[x];
}
复制代码

旋转操作

为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。

旋转需要保证

  • 整棵 Splay 的中序遍历不变(不能破坏二叉查找树的性质)。
  • 受影响的节点维护的信息依然正确有效。
  • root 必须指向旋转后的根节点。

在 Splay 中旋转分为两种:左旋和右旋。

复制代码
inline void xz(int x)//旋转 (保证中序遍历不变)
{
    int y=fa[x],z=fa[y];
    int a=(rc[y]==x?lc[x]:rc[x]);
    fa[x]=z,fa[y]=x;
    if(a) fa[a]=y;
    if(z) (rc[z]==y?rc[z]:lc[z])=x;
    if(rc[y]==x) lc[x]=y,rc[y]=a;
    else rc[x]=y,lc[y]=a;
    siz(y);
}
复制代码

Splay 操作

Splay 操作规定:每访问一个节点  后都要强制将其旋转到根节点。

复制代码
inline void splay(int x,int ro)//旋到对应位置 
{
    while(fa[x]!=ro)
    {
        if(fa[fa[x]]!=ro) 
            wh(fa[x])==wh(x)?xz(fa[x]):xz(x);//同为右(或左)则旋父节点,反之旋自身; 
        xz(x);
    }
    if(!ro) root=x;
    siz(x);
}
复制代码

插入操作

过程

插入操作是一个比较复杂的过程,具体步骤如下(假设插入的值为x):

  • 如果树空了,则直接插入根并退出。
  • 如果当前节点的权值等于x则增加当前节点的大小并更新节点和父亲的信息,将当前节点进行 Splay 操作。
  • 否则按照二叉查找树的性质向下找,找到空节点就插入即可(请不要忘记 Splay 操作)。
复制代码
inline void insert(int v)//插入 
{
    int x=root,y=0,pd;
    while(val[x]!=v&&x)
    {
        si[y=x]++;
        if(val[x]>v) x=lc[x],pd=0;
        else x=rc[x],pd=1;
    }
    if(!x)
    {
        x=++tot;
        val[x]=v,fa[x]=y;
        if(y) (!pd?lc[y]:rc[y])=x;
    }
    si[x]++,cnt[x]++;
    splay(x,0);
}
复制代码

合并两棵树

过程

合并两棵 Splay 树,设两棵树的根节点分别为x和y,那么我们要求x树中的最大值小于y树中的最小值。合并操作如下:

  • 如果x和y其中之一或两者都为空树,直接返回不为空的那一棵树的根节点或空树。
  • 否则将x树中的最大值splay到根,然后把它的右子树设置为y并更新节点的信息,然后返回这个节点。
复制代码
inline void find(int x)//找到值x对应节点并旋成根 
{
    int y=root;
    while(val[y]!=x) 
        y=(x<val[y]?lc[y]:rc[y]);
    splay(y,0);
}

inline int fmax(int x)//找到以x为根的子树的最大值 
{
    while(rc[x])
        x=rc[x];
    return x;
}

inline void join(int x,int y)//合并两棵树 
{
    int z=fmax(x);
    splay(z,0);
    rc[z]=y,fa[y]=z;
    siz(z);
}
复制代码

删除操作

过程

删除操作也是一个比较复杂的操作,具体步骤如下:

首先将  旋转到根的位置。

  • 如果 (有不止一个 ),那么将  减  并退出。
  • 否则,合并它的左右两棵子树即可。
复制代码
inline void de(int x)//删点 
{
    find(x);
    if(cnt[root]>1)//该点值的数量大于1,删一个 
    {
        cnt[root]--,si[root]--;
        return;
    }
    if(si[root]==1)//只有该点,重置树 
        root=0;
    else if(!lc[root]||!rc[root])//去掉该点,根变为右(或左)儿子 
        root=lc[root]+rc[root],fa[root]=0;
    else//删除该点,合并两棵子树 
    {
        fa[lc[root]]=fa[rc[root]]=0;
        join(lc[root],rc[root]);
    }
}
复制代码

查询前驱

过程

前驱定义为小于x的最大的数,那么查询前驱可以转化为:将x插入(此时x已经在根的位置了),前驱即为x的左子树中最右边的节点,最后将x删除即可。

复制代码
inline int qq(int x)//找前驱 
{
    insert(x);
    int y=lc[root];
    while(rc[y]) 
        y=rc[y];
    de(x);
    return y;
}
复制代码

查询后继

过程

后继定义为大于x的最小的数,查询方法和前驱类似:x的右子树中最左边的节点。

 
复制代码
inline int hj(int x)//找后继 
{
    insert(x);
    int y=rc[root];
    while(lc[y]) 
        y=lc[y];
    de(x);
    return y;
}
复制代码

查询 x 的排名

过程

在伸展树中找到x并旋到根(找不到则插入,处理完后删除),返回左子树的size+1;

复制代码
inline void find(int x)//找到值x对应节点并旋成根 
{
    int y=root;
    while(val[y]!=x) 
        y=(x<val[y]?lc[y]:rc[y]);
    splay(y,0);
}

inline int pm(int x)//查x的排名 
{
    find(x);
    return si[lc[root]]+1;
}
复制代码

查询排名 x 的数

过程

设x为剩余排名,具体步骤如下:

  • 如果左子树非空且剩余排名x不大于左子树的大小size,那么向左子树查找。
  • 否则将x减去左子树的和根的大小。如果此时k的值小于等于 ,则返回根节点的权值,否则继续向右子树查找。
复制代码
inline int q(int x)//查排名为x的值 
{
    int y=root;
    for(;;)
    {
        if(x<si[lc[y]]+1) 
            y=lc[y];
        else if(x>si[lc[y]]+cnt[y]) 
            x-=si[lc[y]]+cnt[y],y=rc[y];
        else 
            return y;
    }
}
复制代码

P3369 【模板】普通平衡树

模板题

复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int tot,root,n;
int fa[N],lc[N],rc[N],val[N],cnt[N],si[N];

inline bool wh(int x)//判断是否为右子树; (具体用法见操作splay)
{
    return rc[fa[x]]==x;
}

inline void siz(int x)//计算子树大小size 
{
    si[x]=si[lc[x]]+si[rc[x]]+cnt[x];
}

inline void xz(int x)//旋转 (保证中序遍历不变)
{
    int y=fa[x],z=fa[y];
    int a=(rc[y]==x?lc[x]:rc[x]);
    fa[x]=z,fa[y]=x;
    if(a) fa[a]=y;
    if(z) (rc[z]==y?rc[z]:lc[z])=x;
    if(rc[y]==x) lc[x]=y,rc[y]=a;
    else rc[x]=y,lc[y]=a;
    siz(y);
}

inline void splay(int x,int ro)//旋到对应位置 
{
    while(fa[x]!=ro)
    {
        if(fa[fa[x]]!=ro) 
            wh(fa[x])==wh(x)?xz(fa[x]):xz(x);//同为右(或左)则旋父节点,反之旋自身; 
        xz(x);
    }
    if(!ro) root=x;
    siz(x);
}

inline void insert(int v)//插入 
{
    int x=root,y=0,pd;
    while(val[x]!=v&&x)
    {
        si[y=x]++;
        if(val[x]>v) x=lc[x],pd=0;
        else x=rc[x],pd=1;
    }
    if(!x)
    {
        x=++tot;
        val[x]=v,fa[x]=y;
        if(y) (!pd?lc[y]:rc[y])=x;
    }
    si[x]++,cnt[x]++;
    splay(x,0);
}

inline void find(int x)//找到值x对应节点并旋成根 
{
    int y=root;
    while(val[y]!=x) 
        y=(x<val[y]?lc[y]:rc[y]);
    splay(y,0);
}

inline int fmax(int x)//找到以x为根的子树的最大值 
{
    while(rc[x])
        x=rc[x];
    return x;
}

inline void join(int x,int y)//合并两棵树 
{
    int z=fmax(x);
    splay(z,0);
    rc[z]=y,fa[y]=z;
    siz(z);
}

inline void de(int x)//删点 
{
    find(x);
    if(cnt[root]>1)//该点值的数量大于1,删一个 
    {
        cnt[root]--,si[root]--;
        return;
    }
    if(si[root]==1)//只有该点,重置树 
        root=0;
    else if(!lc[root]||!rc[root])//去掉该点,根变为右(或左)儿子 
        root=lc[root]+rc[root],fa[root]=0;
    else//删除该点,合并两棵子树 
    {
        fa[lc[root]]=fa[rc[root]]=0;
        join(lc[root],rc[root]);
    }
}

inline int qq(int x)//找前驱 
{
    insert(x);
    int y=lc[root];
    while(rc[y]) 
        y=rc[y];
    de(x);
    return y;
}

inline int hj(int x)//找后继 
{
    insert(x);
    int y=rc[root];
    while(lc[y]) 
        y=lc[y];
    de(x);
    return y;
}

inline int pm(int x)//查x的排名 
{
    find(x);
    return si[lc[root]]+1;
}

inline int q(int x)//查排名为x的值 
{
    int y=root;
    for(;;)
    {
        if(x<si[lc[y]]+1) 
            y=lc[y];
        else if(x>si[lc[y]]+cnt[y]) 
            x-=si[lc[y]]+cnt[y],y=rc[y];
        else 
            return y;
    }
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        if(a==1) insert(b);
        if(a==2) de(b);
        if(a==3) printf("%d\n",pm(b));
        if(a==4) printf("%d\n",val[q(b)]);
        if(a==5) printf("%d\n",val[qq(b)]);
        if(a==6) printf("%d\n",val[hj(b)]);
    }
    return 0;
}
复制代码

此外:

splay还可以支持区间翻转操作。

这就需要懒标记出马了:la[i]

懒标记下传

复制代码
inline void pu(int x)//懒标记下传 
{
    if(x&&la[x])
    {
        swap(lc[x],rc[x]);
        la[lc[x]]^=1;
        la[rc[x]]^=1;
        la[x]=0;
    }
}
复制代码

建树

复制代码
inline int bu(int l,int r,int f)//以基础区间建立伸展树 
{
    if(l>r) return 0;
    int mid=(l+r)>>1,x=++tot;
    fa[x]=f,val[x]=a[mid];
    lc[x]=bu(l,mid-1,x),rc[x]=bu(mid+1,r,x);
    siz(x);
    return x;
}
复制代码

旋转和splay操作是一样的

复制代码

  inline bool wh(int x)
  {
      return lc[fa[x]]==x;
  }

inline void xz(int x)
{
    int y=fa[x],z=fa[y];
    int a=(lc[y]==x?rc[x]:lc[x]);
    fa[x]=z,fa[y]=x;
    if(a) fa[a]=y;
    if(z) (lc[z]==y?lc[z]:rc[z])=x;
    if(lc[y]==x) rc[x]=y,lc[y]=a;
    else lc[x]=y,rc[y]=a;
    siz(y);
}

inline void splay(int x,int ro)
{
    while(fa[x]!=ro)
    {
        if(fa[fa[x]]!=ro)
            wh(fa[x])==wh(x)?xz(fa[x]):xz(x);
        xz(x);
    }
    siz(x);
    if(!ro) root=x;
}
复制代码

假设翻转区间[l,r]

则先将l-1旋转到根,再将r+1旋转为根的右儿子。此时,需要翻转的区间就是r+1的左子树,设置懒标记。

另外,我们还需要在整个序列两端加上边界。

假设序列a,那么a[1]=_INT_MAX,a[n+1]=INT_MAX。此时,由于a[1]不再是真正序列的起始位置,所以翻转的区间应为[l+1,r+1]。

复制代码
inline void siz(int x)
{
    si[x]=si[lc[x]]+si[rc[x]]+1;
}

inline int find(int x)//找到对应节点 
{
    int y=root;
    pu(y);
    while(x!=si[lc[y]]+1)
    {
        if(x<si[lc[y]]+1)
            y=lc[y],pu(y);
        else
            x-=si[lc[y]]+1,y=rc[y],pu(y);
    }
    return y;
}

int main()
{
    scanf("%d%d",&n,&m);
    a[1]=-INT_MAX,a[n+2]=INT_MAX;
    for(int i=1;i<=n;i++) a[i+1]=i;
    root=bu(1,n+2,0);
    for(int i=1;i<=m;i++)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        splay(find(l),0);
        splay(find(r+2),root);
        la[lc[rc[root]]]^=1;
    }
    print(root);
}
复制代码

P3391 【模板】文艺平衡树

模板题

复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,tot,root;
int a[N],si[N],val[N],fa[N],lc[N],rc[N];
bool la[N];

inline bool wh(int x)
{
    return lc[fa[x]]==x;
}

inline void siz(int x)
{
    si[x]=si[lc[x]]+si[rc[x]]+1;
}

inline void pu(int x)//懒标记下传 
{
    if(x&&la[x])
    {
        swap(lc[x],rc[x]);
        la[lc[x]]^=1;
        la[rc[x]]^=1;
        la[x]=0;
    }
}

inline int bu(int l,int r,int f)//以基础区间建立伸展树 
{
    if(l>r) return 0;
    int mid=(l+r)>>1,x=++tot;
    fa[x]=f,val[x]=a[mid];
    lc[x]=bu(l,mid-1,x),rc[x]=bu(mid+1,r,x);
    siz(x);
    return x;
}

inline void xz(int x)
{
    int y=fa[x],z=fa[y];
    int a=(lc[y]==x?rc[x]:lc[x]);
    fa[x]=z,fa[y]=x;
    if(a) fa[a]=y;
    if(z) (lc[z]==y?lc[z]:rc[z])=x;
    if(lc[y]==x) rc[x]=y,lc[y]=a;
    else lc[x]=y,rc[y]=a;
    siz(y);
}

inline void splay(int x,int ro)
{
    while(fa[x]!=ro)
    {
        if(fa[fa[x]]!=ro)
            wh(fa[x])==wh(x)?xz(fa[x]):xz(x);
        xz(x);
    }
    siz(x);
    if(!ro) root=x;
}

inline int find(int x)//找到对应节点 
{
    int y=root;
    pu(y);
    while(x!=si[lc[y]]+1)
    {
        if(x<si[lc[y]]+1)
            y=lc[y],pu(y);
        else
            x-=si[lc[y]]+1,y=rc[y],pu(y);
    }
    return y;
}

inline void print(int x)
{
    pu(x);
    if(lc[x]) print(lc[x]);
    if(val[x]^-INT_MAX&&val[x]^INT_MAX) printf("%d ",val[x]);
    if(rc[x]) print(rc[x]);
}

int main()
{
    scanf("%d%d",&n,&m);
    a[1]=-INT_MAX,a[n+2]=INT_MAX;
    for(int i=1;i<=n;i++) a[i+1]=i;
    root=bu(1,n+2,0);
    for(int i=1;i<=m;i++)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        splay(find(l),0);
        splay(find(r+2),root);
        la[lc[rc[root]]]^=1;
    }
    print(root);
}
复制代码


如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
posted @   光暗之影x  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示