浅谈*衡树

吐槽

*代表ping或jin,为什么要屏蔽掉ping和jin

前置知识

二叉排序树没学过的先去学习一下

正文

简介

*衡树是一种可以在O(log n)的时间复杂度上动态插入、删除、求前驱后继、求排名等的树形结构

*衡树的实现方法很多,有Splay、Treap、AVL树、红黑树等

推荐写Treap,因为Treap的常数小,代码复杂度低(相对而言

具体实现

二叉排序树在形为一条链时,有退化至O(n^2)的可能

为了使其稳定在O(n log n),需要使其趋*于*衡状态,如下图

可是如何使链变得*衡呢?

可以进行旋转操作,将子节点转动到父节点,同时保持二叉排序树的性质,如下图

像这样旋转可以在保证性质不变的前提下使其趋*于*衡

旋转分两种,上图为右旋,即将左儿子旋上来,另一种为左旋,即将有儿子旋上来

但是,我们应该在什么时候进行旋转呢?

这就是Treap的精髓,对每一个点附一个随机值,再让随机值保持堆的性质,如果堆的性质被打破,那么旋转

也许你可能很倒霉,随机的值链上就保持堆的性质,然后退化,但因为它是随机的,所以退化的可能性几乎可以忽略不记

实现

P3369 【模板】普通*衡树

#include<bits/stdc++.h>
using namespace std;
struct node{
    long long val,s,m;
    long long x;
    node *l,*r;
}*root;                 //val是值,s是子树大小,m是有几个大小为val的点,x是随机值,l和r是左右子节点
long long s(){
    long long x=rand();
    x*=rand();
    x*=rand();
    x*=rand();
    return x;
}                      //求随机值
void rx(node* &r){
    node *k=r->l;
    r->l=k->r;
    k->r=r;
    r->s=r->s-k->s;
    if(r->l!=NULL) r->s+=r->l->s;
    k->s+=r->s;
    if(r->l!=NULL) k->s-=r->l->s;
    r=k;
}                                      //右旋,看不懂可以画下图
void lx(node* &r){
    node *k=r->r;
    r->r=k->l;                     
    k->l=r;
    r->s=r->s-k->s;
    if(r->r!=NULL) r->s+=r->r->s;
    k->s+=r->s;
    if(r->r!=NULL) k->s-=r->r->s;
    r=k;
}                                  //左旋
void insert(node* &r,long long x){
    if(r==NULL){
        r=new node;
        r->x=s();
        r->val=x;
        r->s=r->m=1;
        r->l=r->r=NULL;
        return;
    }                         //新建节点
    r->s++;                   //子树变大
    if(r->val>x){             //去右儿子
        insert(r->l,x);
        if(r->l->x<r->x) rx(r);        //违背堆的性质,将左儿子右旋上来
    }else if(r->val==x){
        r->m++;                        //值相同,数量增加
        return;
    }else{
        insert(r->r,x);
        if(r->r->x<r->x) lx(r);       //与去右儿子同理,只是反过来    
    }
} 
void cl(node* &r,long long x){
    r->s--;                                    //子树大小减少
    if(r->val>x){
        cl(r->l,x);          
        if(r->l!=NULL && r->l->x<r->x) rx(r);             //要特判空节点
    }else if(r->val==x){                                  //到达需删除的节点
        if(r->m>1){                                         //数量大于1,只减少数量
            r->m--;
            return;
        }
        if(r->l==NULL || r->r==NULL){                   //一个子节点或没有子节点,直接删
            if(r->l==NULL && r->r==NULL) r=NULL;
            else if(r->l==NULL) r=r->r;
            else r=r->l;
            return;
        }else{                                     //两个子节点不能直接删,要旋下去再删
            r->s++;                                //小坑,要对旋上来的节点子树大小减少
            if(r->l->x<r->r->x){
                rx(r);
                r->s--;
                cl(r->r,x);
            }else{
                lx(r);
                r->s--;
                cl(r->l,x);
            }
        }
    }else{
        cl(r->r,x);
        if(r->r!=NULL && r->r->x<r->x) lx(r);
    }
}
long long findp(node* r,long long x){
    if(x<r->val) return findp(r->l,x);                    //在左边就与当前结点和右儿子无关
    else if(x==r->val){
        if(r->l==NULL) return 1;
        else return 1+r->l->s;
    }                                                      //找到后特判左子节点为空,注意不是m而是1,因为是严格排名,相同的不算
    else{
        if(r->l==NULL) return findp(r->r,x)+r->m;
        else return findp(r->r,x)+r->l->s+r->m;
    }                                                     //右子节点把当前节点和左子树算上
}
long long findx(node* r,long long x){
    if(r==NULL) return -1;
    if(x>r->s)  return 0; 
    long long ls;
    if(r->l==NULL) ls=0;
    else ls=r->l->s;
    if(x<=ls) return findx(r->l,x);
    else if(x<=ls+r->m) return r->val;
    else return findx(r->r,x-ls-r->m);                       //跟求排名原理一致,只是倒着做
}
long long findq(node* r,long long x){
    if(r==NULL) return 0;
    if(r->val<x){
        if(r->r!=NULL) return max(r->val,findq(r->r,x));
        else return r->val;                                   //已经比x小,看能否更接*于x,即找右子树
    }else{
        if(r->l!=NULL) return findq(r->l,x);
        else return 0;
    }                                                        //找x的前驱,当前节点更大,就找左子树
}
long long findh(node* r,long long x){
    if(r==NULL) return 1e9;
    if(r->val>x){
        if(r->l!=NULL) return min(r->val,findh(r->l,x));
        else return r->val;
    }else{
        if(r->r!=NULL) return findh(r->r,x);
        else return 1e9;
    }
}                                                             //与前驱同理,只是操作反过来
int main(){
    long long q,opt,x;
    srand(time(0));
    cin>>q;
    while(q--){
        cin>>opt>>x;
        if(opt==1){
            insert(root,x);                 //插入
        }else if(opt==2){
            cl(root,x);                     //删除
        }else if(opt==3){
            cout<<findp(root,x)<<endl;      //输出排名
        }else if(opt==4){
            cout<<findx(root,x)<<endl;      //输出排名为x的数
        }else  if(opt==5){
            cout<<findq(root,x)<<endl;      //求后继
        }else{
            cout<<findh(root,x)<<endl;      //求前驱
        }
    }
    return 0;
}

代码有亿点点长,主要是因为操作类型较多,实际用不着写六种操作

如果用指针写,记得多判空

posted @ 2023-03-22 11:53  蒻蒟cdx  阅读(39)  评论(0编辑  收藏  举报