平衡树 Splay

可以支持的操作:

  1. 插入 xxx 数
  2. 删除 xxx 数(若有多个相同的数,因只删除一个)
  3. 查询 xxx 数的排名(排名定义为比当前数小的数的个数 +1 。若有多个相同的数,因输出最小的排名)
  4. 查询排名为 xxx 的数
  5. xxx 的前驱(前驱定义为小于 xxx ,且最大的数)
  6. xxx 的后继(后继定义为大于 xxx ,且最小的数)

  7.来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1

 0.预备:

struct有:siz,sum(该点出现次数),fa,val,以及ch[0],ch[1]

const int N=100000+10;
const int inf=0x3f3f3f3f;
int n,m;
struct node{
    int siz,sum,val,fa;
    int ch[2];
}t[N];
int pc,dc,dp[N];
int root;

1.pushup,新节点,回收节点

void pushup(int x){
    t[x].siz=t[t[x].ch[0]].siz+t[t[x].ch[1]].siz+t[x].sum;
}
int newnode(int v,int f){
    int r=dc?dp[dc--]:++pc;
    memset(t+r,0,sizeof (node));
    t[r].siz=1,t[r].sum=1,t[r].fa=f,t[r].val=v;
    return r;
}
void del(int x){
    dp[++dc]=x;
}

2.rotate(x),与父亲旋转。注意每一步的转移,注意最后pushup(y)

void rotate(int x){
    int y=t[x].fa,d=t[y].ch[1]==x;
    t[t[y].ch[d]=t[x].ch[d^1]].fa=y;
    t[t[x].fa=t[y].fa].ch[t[t[y].fa].ch[1]==y]=x;
    t[x].ch[d^1]=y,t[y].fa=x;
    pushup(y);
}

 

3.splay(x,f),旋转到f的儿子,双旋操作,一次考虑两个节点。注意,无论如何,rotate(x),无论如何,pushup(x),如果f=0,root=x

 

void splay(int x,int f){
    while(t[x].fa!=f){
        int y=t[x].fa,z=t[y].fa;
        if(z!=f){
            rotate(((t[z].ch[0]==y)==(t[y].ch[0]==x))?y:x);
        }
        rotate(x);
    }
    pushup(x);
    if(f==0) root=x;
}

 

 

 

4.insert(val),注意找到相同的值之后,break掉,注意每次都要splay到根(虽然数据水,不旋更快),理论保证树高。

void insert(int val){
    if(!root){
        root=newnode(val,0);
        return;
    }
    int u=root;
    while(1){
        t[u].siz++;
        if(t[u].val==val){
        t[u].sum++;
        break;
        }
        int d=t[u].val<val;
        if(!t[u].ch[d]){
            t[u].ch[d]=newnode(val,u);
            u=t[u].ch[d];
            break;
        }
        u=t[u].ch[d];
    }
    //splay(u,0);
}

 

5.dele(val),步骤:先找到val,旋转到根,再找到根节点右子树最小值,即root的后继,splay到根的儿子(必然是右儿子),再删掉root,改root为右儿子

需要特判:右子树不存在?直接将左二子当做新根。删掉的是最后一个值?我这个代码不怕,root会直接变成0

void dele(int val){
    int goal=root,son=0;
    while(1){
        if(t[goal].val==val) break;
        goal=t[goal].ch[t[goal].val<val];
    }
    t[goal].sum--;
    splay(goal,0);
    t[goal].siz--;
    if(t[goal].sum>0) return;
    son=t[goal].ch[1];
    while(son&&t[son].ch[0]){
        son=t[son].ch[0];
    }
    if(son){
        splay(son,root);
        t[son].ch[0]=t[root].ch[0];
        t[t[root].ch[0]].fa=son;
        del(root);
        root=son;
        t[son].fa=0;
        pushup(son);
    }
    else{
        root=t[goal].ch[0];
        t[root].fa=0;
        del(goal);
    }
}

 

6.rank(x),同treap

7.kth(k),同treap

8.front(x),同treap

9.back(x),同treap

int rank(int val){
    int u=root;
    int ret=1;
    while(u){
        if(t[u].val<val) {
            ret+=t[t[u].ch[0]].siz+t[u].sum;
            u=t[u].ch[1];
        }
        else if(t[u].val==val) {ret+=t[t[u].ch[0]].siz;break;}
        else u=t[u].ch[0]; 
    }
    return ret;
} 
int kth(int k){
    int u=root;
    while(1){
        if(!u) return 0;
        int d=k-t[t[u].ch[0]].siz;
        if(d<=0) u=t[u].ch[0];
        else if(d>=1&&d<=t[u].sum) return t[u].val;
        else {
            k=d-t[u].sum;
            u=t[u].ch[1];
        }
    }
}
int front(int val){
    int u=root;
    int ret=-inf;
    while(u){
        if(t[u].val<val){
            ret=max(ret,t[u].val);
            u=t[u].ch[1];
        }
        else {
            u=t[u].ch[0];
        }
    }
    return ret;
}
int back(int val){
    int u=root;
    int ret=inf;
    while(u){
        if(t[u].val>val){
            ret=min(ret,t[u].val);
            u=t[u].ch[0];
        }
        else{
            u=t[u].ch[1];
        }
    }
    return ret;
}

 

10.pushdown(x),翻转其实就是不停地交换节点的两个子树。因为在每次访问到这个节点的时候,尤其在splay的时候,必然会启动pushdown,所以,翻转最终一定会都实现。下放旋转标记,经常勤调用,要记得。

11.work(l,r),处理旋转操作,这里,为了避免l=1,r=n的情况,放置1,n+2两个哨兵,每个节点的编号实际上是编号减一。最后输出要减一。

这个方法是针对1~n的排列,如果不是,最好是特判,其实加哨兵也是可以的。

实现的时候,先找到kth(l)也就是第k大的值,体现在中序遍历里就是这个点的编号。当然,其实找的是l-1,但是由于编号是从2开始的。

将l旋转到根。

同理,r=kth(r+2)。

将r旋转到根节点的儿子(必然是右儿子)

这样,根节点的右儿子的左子树就是所求区间。

12.write(o),记得pushdown,输出的编号一定要在[2,n+1]内,因为1,n+2都是哨兵。按照中序遍历输出即可

 

对于一个需要维护区间操作的平衡树来说,每个节点的排序方式是编号大小,也就是中序遍历的节点编号一定是一个从1~n的单增序列。保证可以取l-1到根节点,r+1到根节点的儿子,就可以取出[l,r]了。

注意事项:

1.rotate里面的压行,不要记错,不行就手写。其实,第一行换儿子,第二行改父亲,第三行改两点之间的关系。

2.插入元素的时候,如果用while1循环,记得每到一个节点, 那么这个节点的总的siz必然要加一,不论是新赋值,还是多了一个新值。

3.插入元素的时候,如果找到了这个值,那么就要break,不能继续走了。

4.删除的时候找右子树中的最大值,为了防止哨兵出了问题,应改写为:

while(son&&t[son].ch[0]){
        son=t[son].ch[0];
    }

5.区间反转的时候,kth,write,以及各种访问到这个点的时候,都要pushdown

 

转载于:https://www.cnblogs.com/Miracevin/p/9102198.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值