无旋Treap学习笔记+例题

本文介绍了无旋Treap数据结构,一种结合二叉搜索树和堆特性的数据结构。文章详细讲解了其核心操作Merge和Split,以及如何利用它们实现Insert、Delete、Build等基本操作。此外,还提供了[BZOJ3224]普通平衡树的解题思路和代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.背景

    由于我被那些转来转去的平衡树转得头昏脑涨,于是就想学学不需要旋转的无旋Treap(毕竟无旋Treap还可以可持久化,感觉挺棒),经过半个小时的理解和一个小时的码代码+调试,终于将例题AC..(不过常数好像有点大..是我原来写的替罪羊树的3倍)。

2.介绍

    Treap,他的名称的由来就是Tree+heap,它是一颗同时具备二叉搜索树和堆的性质的一颗二叉树,并且他是通过rand一个数来维护保证树高为O(logn)的。它的两大核心操作就是Split和Merge,所有用它来完成的修改操作都需要通过这两大操作来完成。接下来就简要说说这两大操作。(P.S. 这下面的部分代码是有Pushdown函数的,不过如果你的操作中不会出现打标记的情况的话,是不用Pushdown操作的。)

2.1 Merge

    Merge函数是将两个平衡树合并为一个平衡树,用法是int Merge(int a,int b),传入的是两颗需要合并的树的根,返回合并后的那棵树的根。但是有一点要注意的是,Merge两颗root分别为a,b的树需要有一个前提,tree(a)中最大的数要小于tree(b)中最小的数。之后操作其实非常简单,如果a点的rand值小于b点,那么就Merge(a.rs,b),然后将返回的值定为a的rs,否则就Merge(a,b.ls),将返回的值定为b的ls,这样可以很好地维护树高。

2.1.1 Code

inline int Merge(int a,int b) {
    if(!a||!b)
        return a|b;
    Pushdown(a);
    Pushdown(b);
    if(tr[a].fix<tr[b].fix) {
        tr[a].rs=Merge(tr[a].rs,b);
        Pushup(a);
        return a;
    }
    else {
        tr[b].ls=Merge(a,tr[b].ls);
        Pushup(b);
        return b;
    }
}

2.2 Split

    Split函数是将一个平衡树分割成两个平衡树,用法是pair Split(int o,int k),这里传入的是需要Split的树的根和需要将前k个节点分离出去的k,返回的是分离后的两个平衡树的根。如果当前o的左子树的size恰好等于k,那么我们直接将左子树分离出去就行了;如果当前o的左子树的size+1恰好等于k,那么我们可以直接将右子树分离出去。否则就递归进行:如果左子树的size>k,那么递归到左子树继续分割,否则到右子树进行分割。不过在右子树分割的时候要注意递归时k应变成k-左子树size-1。并且注意Split完后要将分离掉的那边的孩子设为0。

2.2.1 Code(Split函数写得有点丑见谅qwq)

inline pair<int,int> Split(int o,int k) {
    if(!o)
        return make_pair(0,0);
    Pushdown(o);
    if(Sz(tr[o].ls)==k) {
        int pre=tr[o].ls;
        tr[o].ls=0;
        Pushup(o);
        return make_pair(pre,o);
    }
    if(Sz(tr[o].ls)+1==k) {
        int pre=tr[o].rs;
        tr[o].rs=0;
        Pushup(o);
        return make_pair(o,pre);
    }
    if(Sz(tr[o].ls)>k) {
        pair<int,int>tmp=Split(tr[o].ls,k);
        tr[o].ls=tmp.second;
        Pushup(o);
        return make_pair(tmp.first,o);
    }
    pair<int,int>tmp=Split(tr[o].rs,k-Sz(tr[o].ls)-1);
    tr[o].rs=tmp.first;
    Pushup(o);
    return make_pair(o,tmp.second);
}

3.基本操作

    介绍完两大核心操作后,接下来来讲讲如何利用这两个操作进行其他的操作。(P.S. 下文的cnt表示现在一共有cnt个节点)

3.0 一些函数的声明

//cnt为节点个数
//给每个节点赋值一个值来维护树
inline int Rand() {
    return seed=(int)(1ll*seed*base%INT_MAX);
}
//节点回收(没有卡内存的话其实可以不用)
inline void Recycle(int x) {
    rec[++rtp]=x;
}
struct Treap {
    int sz,ls,rs,val,fix;
    Treap(int _val=0):val(_val){ls=rs=sz=0;fix=Rand();}//需要建新节点的时候直接Treap(val)就行了
}tr[maxn];
//需要节点回收时使用。
inline void Clear(int o) {
    if(!o)
        return;
    Clear(tr[o].ls);
    Clear(tr[o].rs);
    Recycle(o);
}
//有节点回收的Newnode
inline int Newnode(int val) {
    int tmp=rtp?rec[rtp--]:++cnt;
    tr[tmp]=Treap(val);
    tr[tmp].sz=1;
    return tmp;
}
//无节点回收的Newnode
inline int Newnode(int val) {
    tr[++cnt]=Treap(val);
    return cnt;
}
//判断这个节点是否是空节点,如果是空节点不需要上传。
inline int Sz(int o) {
    return !o?0:tr[o].sz;
}
//更新子树sz和其他一些需要维护的东西
inline void Pushup(int o) {
    tr[o].sz=Sz(tr[o].ls)+Sz(tr[o].rs)+1;
}
//给一个节点赋值
inline void Cover(int o,int x) {
    tr[o].sum=x*tr[o].sz;
    tr[o].val=x;tr[o].cov=x;
}
//翻转一个区间的两个孩子
inline void Revnode(int o) {
    swap(tr[o].ls,tr[o].rs);
    tr[o].rev^=1;
}
//Pushdown来下传标记(用区间反转标记和区间赋值标记来举例)
inline void Pushdown(int o) {
    if(!o)
        return;
    if(tr[o].rev) {
        if(tr[o].ls)
            Revnode(tr[o].ls);
        if(tr[o].rs)
            Revnode(tr[o].rs);
        tr[o].rev=0;
    }
    if(tr[o].cov!=-2333) {
        if(tr[o].ls)
            Cover(tr[o].ls,tr[o].cov);
        if(tr[o].rs)
            Cover(tr[o].rs,tr[o].cov);
        tr[o].cov=-2333;
    }
}

3.1 Insert

    当我们想要插入一个值的时候,我们先查询这个值的rank,然后将原树分离成1~rank和rank+1~cnt两颗树,之后新建一个节点,再进行两次Merge将这个节点并入原来的节点中(root=Merge(Tree(1~rank),newnode),root=Merge(root,Tree(rank+1~cnt))),注意两次Merge完都要将root重新赋值。

3.1.1 Code

//这是插入在pos位后插入长度为k的一个序列
inline void Insert(int o,int pos,int k) {
    Pushdown(o);
    for(int i=1;i<=k;i++)
        read(a[i]);
    int now=Build(a,k);
    pair<int,int>tmp=Split(o,pos);
    rt=Merge(Merge(tmp.first,now),tmp.second);
}
//这是在第num位后插入一个数
inline void Insert(int o,int num) {
    int rnk=Rank(o,num);
    pair<int,int>tmp=Split(o,rnk);
    tr[++cnt]=node(0,0,num,1);
    rt=Merge(tmp.first,cnt);
    rt=Merge(rt,tmp.second);
}

3.2 Delete

    当我们想要删除一个值的时候,我们也是先查询这个值的rank,然后将原树分成3部分,1~rank-1,rank,rank+1~cnt,就是调用两次Split(pair tmp1=Split(root,rank),tmp2=Split(tmp1.first,rank-1)),之后我们再Merge(tmp2.first,tmp1.second)就行了,这样我们就能很好地将这个节点删除而不破坏原树的性质了。

3.2.1 Code

//关于Delete..如果题目卡内存的话,你需要Clear并回收节点(详细请看我的维护数列的那道题的Code)
inline void Delete(int o,int l,int r) {
    Pushdown(o);
    pair<int,int>tmp1=Split(o,r),tmp2=Split(tmp1.first,l-1);
    Clear(tmp2.second);
    rt=Merge(tmp2.first,tmp1.second);
}

3.3 Build

    如果我们已经有了一个序列,然后需要将他插入到Treap中,那么我们先将这个序列单独建成一颗Treap之后再合并就行了,而这时候我们就需要一个叫做Build的函数来建树,在建树过程中,我们需要一个栈,栈中存着这个需要插入的序列中的已经被插入的节点,当我们有新节点y进入的时候,我们要在栈中找到第一个fix值小于这个节点的节点x(前面那些大于他的已经被pop出去了,注意在pop一个节点的时候我们需要将他Update一遍,更新节点信息),之后将新加的这个点作为x的右孩子,然后最后一个在栈中fix值大于节点y的fix值的节点就作为y的左孩子。再将节点y放到栈里就行了。最后返回的值为这个新树的根。

3.3.1 Code

inline int Build(int *v,int len) {
    tp=0;
    for(int i=1;i<=len;i++) {
        int now=Newnode(v[i]),lst=0;
        while(tp&&tr[stk[tp]].fix>tr[now].fix) {
            Pushup(stk[tp]);
            lst=stk[tp];
            stk[tp--]=0;
        }
        if(tp)
            tr[stk[tp]].rs=now;
        tr[now].ls=lst;
        stk[++tp]=now;
    }
    while(tp)
        Pushup(stk[tp--]);
    return stk[1];
}

3.4 Getkth&&Getrank&&prev(前驱)&&succ(后继)

    这一部分的这4个操作,由于它是一颗平衡树,所以只需要暴力找就行了,reverse的话,打个标记然后pushdown就行了。

3.4.1 Code

//求前驱
inline int Prev(int o,int k) {
    int res=-1e9;
    while(o)
        if(tr[o].val<k)
            res=max(res,tr[o].val),o=tr[o].rs;
        else
            o=tr[o].ls;
    return res;
}
//求后继
inline int Succ(int o,int k) {
    int res=1e9;
    while(o)
        if(tr[o].val>k)
            res=min(res,tr[o].val),o=tr[o].ls;
        else
            o=tr[o].rs;
    return res;
}
//求一个数在Treap中的排名
inline int Rank(int o,int k) {
    int res=0,t=1e9;
    while(o) {
        if(tr[o].val==k)
            t=min(t,res+tr[tr[o].ls].sz+1);
        if(tr[o].val<k)
            res+=tr[tr[o].ls].sz+1,o=tr[o].rs;
        else
            o=tr[o].ls;
    }
    return t==1e9?res:t;
}
//求一个排名为k的数的值
inline int Num(int o,int k) {
    while(1)
        if(tr[tr[o].ls].sz+1==k)
            return tr[o].val;
        else if(tr[tr[o].ls].sz+1<k)
            k-=tr[tr[o].ls].sz+1,o=tr[o].rs;
        else
            o=tr[o].ls;
}

3.5 区间操作:Reverse(翻转)&&Cover(赋值)

    对于所有的区间操作,用无旋Treap写都是非常简单的,你只需要通过两次Split分离出这个l~r的区间,然后给这个区间打上标记就行了(不要忘记Merge回去)。

3.5.1 Code

//区间赋值
inline void Makesame(int o,int l,int r,int k) {
    Pushdown(o);
    pair<int,int>tmp1=Split(o,r),tmp2=Split(tmp1.first,l-1);
    Cover(tmp2.second,k);
    rt=Merge(Merge(tmp2.first,tmp2.second),tmp1.second);
}
//区间反转
inline void Reverse(int o,int l,int r) {
    Pushdown(o);
    pair<int,int>tmp1=Split(o,r),tmp2=Split(tmp1.first,l-1);
    Revnode(tmp2.second);
    rt=Merge(Merge(tmp2.first,tmp2.second),tmp1.second);
}

3.6 敬请期待..

    还有一些操作我还不知道,等我会了之后再更新吧。

4.例题

4.1 [BZOJ3224]普通平衡树

4.1.1 思路

    这题..就是个模板题吧,没什么好说的,直接扔模板就好辣!

4.1.2 Code

#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
bool Finish_read;
template<class T>inline void read(T &x){Finish_read=0;x=0;int f=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-')f=-1;if(ch==EOF)return;ch=getchar();}while(isdigit(ch))x=x*10+ch-'0',ch=getchar();x*=f;Finish_read=1;}
template<class T>inline void print(T x){if(x/10!=0)print(x/10);putchar(x%10+'0');}
template<class T>inline void writeln(T x){if(x<0)putchar('-');x=abs(x);print(x);putchar('\n');}
template<class T>inline void write(T x){if(x<0)putchar('-');x=abs(x);print(x);}
/*================Header Template==============*/
const int base=48271,maxn=100005;
int rt,seed=233,cnt;
inline int Rand() {
    return seed=(int)(1ll*seed*base%INT_MAX);
}
struct node {
    int ls,rs,fix,val,sz;
    node(int _ls=0,int _rs=0,int _val=0,int _sz=0):ls(_ls),rs(_rs),val(_val),sz(_sz){fix=Rand();}
}tr[maxn];
inline void update(int o) {
    tr[o].sz=tr[tr[o].ls].sz+tr[tr[o].rs].sz+1;
}
inline int Merge(int a,int b) {
    if(!a||!b)
        return a|b;
    if(tr[a].fix<tr[b].fix) {
        tr[a].rs=Merge(tr[a].rs,b);
        update(a);
        return a;
    }
    else {
        tr[b].ls=Merge(a,tr[b].ls);
        update(b);
        return b;
    }
}
inline pair<int,int> Split(int o,int k) {
    if(!o)
        return make_pair(0,0);
    if(tr[tr[o].ls].sz==k) {
        int pre=tr[o].ls;
        tr[o].ls=0;
        update(o);
        return make_pair(pre,o);
    }
    if(tr[tr[o].ls].sz+1==k) {
        int pre=tr[o].rs;
        tr[o].rs=0;
        update(o);
        return make_pair(o,pre);
    }
    if(tr[tr[o].ls].sz>k) {
        pair<int,int> tmp=Split(tr[o].ls,k);
        tr[o].ls=tmp.second;
        update(o);
        return make_pair(tmp.first,o);
    }
    pair<int,int> tmp=Split(tr[o].rs,k-tr[tr[o].ls].sz-1);
    tr[o].rs=tmp.first;
    update(o);
    return make_pair(o,tmp.second);
}
inline int Prev(int o,int k) {
    int res=-1e9;
    while(o)
        if(tr[o].val<k)
            res=max(res,tr[o].val),o=tr[o].rs;
        else
            o=tr[o].ls;
    return res;
}
inline int Succ(int o,int k) {
    int res=1e9;
    while(o)
        if(tr[o].val>k)
            res=min(res,tr[o].val),o=tr[o].ls;
        else
            o=tr[o].rs;
    return res;
}
inline int Rank(int o,int k) {
    int res=0,t=1e9;
    while(o) {
        if(tr[o].val==k)
            t=min(t,res+tr[tr[o].ls].sz+1);
        if(tr[o].val<k)
            res+=tr[tr[o].ls].sz+1,o=tr[o].rs;
        else
            o=tr[o].ls;
    }
    return t==1e9?res:t;
}
inline int Num(int o,int k) {
    while(1)
        if(tr[tr[o].ls].sz+1==k)
            return tr[o].val;
        else if(tr[tr[o].ls].sz+1<k)
            k-=tr[tr[o].ls].sz+1,o=tr[o].rs;
        else
            o=tr[o].ls;
}
inline void Insert(int o,int num) {
    int rnk=Rank(o,num);
    pair<int,int>tmp=Split(o,rnk);
    tr[++cnt]=node(0,0,num,1);
    rt=Merge(tmp.first,cnt);
    rt=Merge(rt,tmp.second);
}
inline void Delete(int o,int num) {
    int rnk=Rank(o,num);
    pair<int,int>tmp1=Split(o,rnk),tmp2=Split(tmp1.first,rnk-1);
    rt=Merge(tmp2.first,tmp1.second);
}
int main() {
    int q;
    read(q);
    while(q--) {
        int op,x;
        read(op);read(x);
        if(op==1)
            Insert(rt,x);
        if(op==2)
            Delete(rt,x);
        if(op==3)
            printf("%d\n",Rank(rt,x));
        if(op==4)
            printf("%d\n",Num(rt,x));
        if(op==5)
            printf("%d\n",Prev(rt,x));
        if(op==6)
            printf("%d\n",Succ(rt,x));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值