平衡树

基础操作

New_node

新建一个节点,然后没了。

inline int New_node(int val){
    tr[++tot].val=val;
    tr[tot].dat=rand();
    tr[tot].siz=1;
    return tot;
}

Split

最主要的有按权值分裂和按排名分裂两种。

按权值分裂

维护权值大小关系常用。

对于某个特定权值 \(val\) , 若节点 \(rt\) 的权值 \(\le val\) ,那么左子树的所有权值也一定 \(\le val\)。将左区间树的根节点 \(x\) 赋为 \(rt\) ,并对其建立虚拟节点 \(rson(rt)\),往右子树递归。反之,将右区间树的根节点 \(y\) 赋为 \(rt\) ,并对其建立虚拟节点 \(lson(rt)\),往左子树递归。

直到 \(rt\) 为空时递归终止,\(x,y\) 都赋为 \(0\)

注意返回前要在最后 \(\operatorname{pushup}\)

inline void Split(int rt,int val,int &x,int &y){
    if(!rt) return x=0,y=0,void();
    if(tr[rt].val<=val) x=rt,Split(rson(rt),val,rson(rt),y);
    else y=rt,Split(lson(rt),val,x,lson(rt));
    pushup(rt);
}

按排名分裂

维护序列操作常用。

对于某个特定排名 \(k\) ,要让排名小于等于 \(k\) 的节点归为左区间树,否则归为右区间树,整个流程与按权值分裂差不多。唯一一点不同的是应该按照根节点的排名进行判断。若 \(k>tr[lson(rt)].siz\),根节点归为左区间树,并在右子树内按 \(k-tr[lson(rt)].siz-1\) 进行分裂,反之同理。

inline void Split(int rt,int k ,int &x,int &y){
    if(!rt) return x=0,y=0,void();
    if(k>tr[lson(rt)].siz) x=rt,Split(rson(rt),k-tr[lson(rt)].siz-1,rson(rt),y);
    else y=rt,Split(lson(rt),val,x,lson(rt));
    pushup(rt);
}

Merge

对于两棵树的根节点 \(u\) (左)和 \(v\) (右),为保证合并之后中序遍历不变,只有两种选择:一种是将 \(u\) 作为根,并继续向下合并 \(v\)\(u\) 的右子树,另一种是将 \(v\) 作为根,并继续向下合并 \(u\)\(v\) 的左子树。同时为保证合并之后的平衡结构,每个节点还需要一个随机值 \(dat\),通过维护一个 \(dat\) 的小/大根堆使合并之后的二叉树趋近平衡。递归到某一节点为空时返回另一个为空/不为空的节点。

注意返回前要在最后 \(\operatorname{pushup}\)

inline int Merge(int u,int v){
    if(!u||!v) return u|v;
    if(tr[u].dat<tr[v].dat){
        rson(u)=Merge(rson(u),v);
        pushup(u);
        return u;
    }else{
        lson(v)=Merge(u,lson(v));
        pushup(v);
        return v;
    }
}

Findsiz

查找特定节点的排名。

对于某个特定节点 \(rt\) ,初始排名为 \(tr[lson(rt)].siz+1\)。对比查询排名为 \(k\)的节点的操作,显然,如果 \(rt\)\(fa(rt)\) 的左儿子,此时往上跳父亲对排名来讲不会有任何改变,若为右儿子,排名需要加上 \(tr[lson(fa(rt))].siz+1\)。不断往上跳父亲,直到 \(fa(rt)\) 为根节点为止。

由于树高是 $ \log n$ 的,所以最多会跳 \(\log n\) 次。

inline int Findsiz(int rt){
    int res=tr[lson(rt)].siz+1;
    while(rt!=root){
        if(rt==rson(fa(rt))) res+=tr[lson(fa(rt))].siz+1;
        rt=fa(rt);
    }
    return res;
}

Insert

inline void Insert(int val){
    int x=0,y=0;
    Split(root,val,x,y);
    root=Merge(Merge(x,New_node(val)),y);
}

Delete

注意删除某一个该权值的节点与删除全部该权值节点的不同。

inline void Delete(int val){
    int x=0,y=0,z=0;
    Split(root,val,x,y);
    Split(x,val-1,x,z);
    /*
    删除一个该权值的节点
    z=Merge(lson(z),rson(z));
    x=Merge(x,z);
    root=Merge(x,y);
    */
    /*
    删除全部该权值的节点
    root=Merge(x,y);
    */
} 

例题

现在你已经学会 \(\operatorname{FHQ\_Treap}\) 的基础操作了,快来切几道平衡树板子题吧(大雾

[ZJOI2006]书架

题意:

洛谷P2596

解题思路:

确实是平衡树裸题,很简单,没有一点难度。

[ZJOI2007]报表统计

题意:

洛谷P1110

解题思路:

\(\huge{\textit{我他妈手敲一百八十行线段树和平衡树啊啊啊啊}}\)

好吧这不是最主要的。

\(\huge{\textit{但是题解告诉我能用两个multiset水过去啊啊啊啊}}\)

发现题意依然操作某个序列,但后面两个查询让我们意识到事情并不是很对劲。

MIN_GAP:查询相邻元素的最小差值

MIN_SORT_GAP:查询所有元素之间的最小差值

继续观察,又发现其实原序列根本不需要维护,原序列的更改只会删除一个原位置上两个相邻元素的差值,同时新增加两个差值。我们完全可以对差值的大小关系维护一个平衡树,每次查询 $ \operatorname{MINGAP}$ 相当于查询平衡树中排名为 \(1\) 的权值。

对于第二类的询问,某人神必地用了一个线段树来实现。首先离散化所有的插入值。插入即是在线段树的某个位置进行更新。更新到一个位置,我们可以去找前面离他最近的位置(最大)和后面离他最近的位置(最小)去更新答案,所以线段树中要维护区间最大值和最小值。其实也不难写

(大恼

my code
#include <cmath>
#include <cstdio>
#include <random>
#include <algorithm>
#define lson(x) tr[x].ls
#define rson(x) tr[x].rs
#define segls (rt<<1)
#define segrs (rt<<1|1)
#define Reg register
using namespace std;
const int maxn=1100100,inf=2147483100;
int n,m,tot,root,atot,mingapans=inf;
int a[maxn],ark[maxn],last[maxn];
int opt[maxn][3];
char copt[30]; 
struct EE{
    int l,r,maxx,minn;
}segtr[maxn<<2];
struct FHQ_Treap{
    int ls,rs,dat,val,siz;
}tr[maxn];
inline int read(){
    int s=0,w=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') w=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        s=(s<<1)+(s<<3)+(ch^48);
        ch=getchar();
    }
    return s*w;
}
inline void pushup(int rt){
    tr[rt].siz=tr[lson(rt)].siz+tr[rson(rt)].siz+1;
}
inline int New_node(int val){
    tr[++tot].val=val;
    tr[tot].siz=1;
    tr[tot].dat=rand();
    return tot;
}
inline void Spilt(int rt,int val,int &x,int &y){
    if(!rt) return x=0,y=0,void();
    if(tr[rt].val<=val) x=rt,Spilt(rson(rt),val,rson(rt),y);
    else y=rt,Spilt(lson(rt),val,x,lson(rt));
    pushup(rt);
}
inline int Merge(int u,int v){
    if(!u||!v) return u|v;
    if(tr[u].dat<tr[v].dat){
        rson(u)=Merge(rson(u),v);
        pushup(u);
        return u;
    }else{
        lson(v)=Merge(u,lson(v));
        pushup(v);
        return v;
    }
}
inline int findid(int rt,int rank){
    if(rank<=tr[lson(rt)].siz) return findid(lson(rt),rank);
    else if(rank==tr[lson(rt)].siz+1) return tr[rt].val;
    else return findid(rson(rt),rank-tr[lson(rt)].siz-1);
}
inline void Seg_pushup(int rt){
    segtr[rt].maxx=max(segtr[segls].maxx,segtr[segrs].maxx);
    segtr[rt].minn=min(segtr[segls].minn,segtr[segrs].minn);
}
inline void Seg_build(int rt,int l,int r){
    segtr[rt].l=l;
    segtr[rt].r=r;
    segtr[rt].minn=inf;
    if(l==r) return;
    int mid=(l+r)>>1;
    Seg_build(segls,l,mid);
    Seg_build(segrs,mid+1,r);
}
inline void update(int rt,int l,int r,int pos){
    if(segtr[rt].l==segtr[rt].r){
        segtr[rt].maxx=segtr[rt].minn=pos;
        return;
    }
    int mid=(segtr[rt].l+segtr[rt].r)>>1;
    if(pos<=mid) update(segls,l,r,pos);
    else update(segrs,l,r,pos);
    Seg_pushup(rt);
}
inline int querymin(int rt,int l,int r){
    if(l<=segtr[rt].l&&segtr[rt].r<=r) return segtr[rt].minn;
    int mid=(segtr[rt].l+segtr[rt].r)>>1,minn=inf;
    if(l<=mid) minn=min(minn,querymin(segls,l,r));
    if(r>mid) minn=min(minn,querymin(segrs,l,r));
    return minn;
}
inline int querymax(int rt,int l,int r){
    if(l<=segtr[rt].l&&segtr[rt].r<=r) return segtr[rt].maxx;
    int mid=(segtr[rt].l+segtr[rt].r)>>1,maxx=0;
    if(l<=mid) maxx=max(maxx,querymax(segls,l,r));
    if(r>mid) maxx=max(maxx,querymax(segrs,l,r));
    return maxx;
}
inline void Insert(int val){
    int x=0,y=0;
    Spilt(root,val,x,y);
    x=Merge(x,New_node(val));
    root=Merge(x,y);
}
inline void Delete(int val){
    int x=0,y=0,z=0;
    Spilt(root,val,x,y);
    Spilt(x,val-1,x,z);
    z=Merge(lson(z),rson(z));
    x=Merge(x,z);
    root=Merge(x,y);
}
int fl;
inline void Insertst(int val,int pos){
    int z=0,t=0,l=0;
    if(pos+1<=n) l=abs(ark[a[pos+1]]-last[pos]),Delete(l);
    z=abs(last[pos]-val),Insert(z);
    if(pos+1<=n) t=abs(ark[a[pos+1]]-val),Insert(t);
    last[pos]=val;
    val=lower_bound(ark+1,ark+1+atot,val)-ark;
    int lmax=0,rmin=inf;
    lmax=querymax(1,1,val); 
    if(val<atot) rmin=querymin(1,val+1,atot);
    if(lmax!=0) mingapans=min(mingapans,abs(ark[val]-ark[lmax]));
    if(rmin!=inf) mingapans=min(mingapans,abs(ark[val]-ark[rmin]));
    update(1,1,n,val);
}
int main(){
    srand(time(0));
    n=read(),m=read();atot=n;
    for(Reg int i=1;i<=n;++i){
        a[i]=ark[i]=read();
        if(i>=2) Insert(abs(a[i]-a[i-1]));
    }
    for(Reg int i=1;i<=m;++i){
        scanf("%s",copt+1);
        if(copt[1]=='I'){
            opt[i][0]=1;
            opt[i][1]=read();
            opt[i][2]=ark[++atot]=read();
        }else if(copt[5]=='G') opt[i][0]=2;
        else opt[i][0]=3;
    }
    sort(ark+1,ark+1+atot);
    atot=unique(ark+1,ark+1+atot)-ark-1;
    Seg_build(1,1,atot);
    int lmax=0,rmin=inf;
    for(Reg int i=1;i<=n;++i){
        last[i]=a[i];
        a[i]=lower_bound(ark+1,ark+1+atot,a[i])-ark;
        lmax=0,rmin=inf;
        lmax=querymax(1,1,a[i]); 
        if(a[i]<atot) rmin=querymin(1,a[i]+1,atot);
        if(lmax!=0) mingapans=min(mingapans,abs(ark[a[i]]-ark[lmax]));
        if(rmin!=inf) mingapans=min(mingapans,abs(ark[a[i]]-ark[rmin]));
        update(1,1,n,a[i]);
    }
    for(Reg int i=1;i<=m;++i){
        if(opt[i][0]==1)Insertst(opt[i][2],opt[i][1]);
        else if(opt[i][0]==2) printf("%d\n",findid(root,1));
        else printf("%d\n",mingapans);
    }
    return 0;
}

[CQOI2014]排序机械臂

题意:

洛谷P3165

解题思路:

比较裸的文艺平衡树。

发现平衡树想要翻转区间只需要交换左右儿子就可以了,但对于每一次操作不能递归到低,只能打懒标记。

下发懒标记( \(\operatorname{pushdown}\) )的一些注意事项:

  • 进行分裂操作时要先 \(\operatorname{pushdown}\) 再递归分裂,原因显然,否则会影响到左右儿子信息的更改。

  • 进行合并操作时要先 \(\operatorname{pushdown}\) 再进行合并。

  • 查询特定节点的排名时( \(\operatorname{Findsiz}\) ),先要递归所要查询节点 \(rt\) 的父亲,从根节点逐一 \(\operatorname{pushdown}\)\(rt\) ,保证所查询的信息都是更改过后的。

跳父亲时有一定的概率陷入死循环(麻),这时候进行分裂操作时要将 \(fa(x),fa(y),fa(z)\) 都赋为 \(0\)但实测仍有一定的概率陷入死循环

[JSOI2008]火星人

题意:

洛谷P4036

解题思路:

题库里用 \(\operatorname{vector}\) 能卡过,当然官方数据就不行了。

考虑用平衡树维护字符串序列,并在树中维护子串的哈希值。

显然,\(\operatorname{pushup}\) 时应有下式:( \(f\) 为哈希值,\(c\) 为节点保存的字符)

\[tr[rt].f=tr[lson(rt)].f\times p_{tr[rson(rt)].siz+1}+tr[rt].c \times p_{tr[rson(rt)].siz}+tr[rson(rt)].f \]

插入和修改操作比较好搞,重要的是该怎么查询两个串的最长公共前缀( \(\operatorname{LCP}\) )。

发现二分+哈希可以解决,对 \(\operatorname{LCP}\) 的长度进行二分,需要判断的时候就把相应的区间分裂下来,比较两个区间的哈希值是否相等即可,单次查询复杂度 \(O(\log^2n)\)

[TJOI2019]甲苯先生的滚榜

题意:

要求维护一个 \(\operatorname{OJ}\) 的排行榜,这个排行榜由 \(\operatorname{AC}\) 题数和罚时组成。现在有 \(m\) 个人和 \(n\)\(\operatorname{AC}\),每次通过种子随机出一个人的编号 \(Ria\) 和罚时 \(Rib\) ,指编号为 \(Ria\) 的人又 \(\operatorname{AC}\) 了一道题,罚时加上 \(Rib\),询问每一次 \(\operatorname{AC}\) 之后有多少人排在这个人前面。

\(m\le 1e5,n\le 1e6\)

解题思路:

显然,通过题数越多,排名越靠前,罚时越少,排名越靠前,这样以来我们就有两个关键字。

首先按排名分裂出该编号的节点 \(z\) (如果没有就新建),再按照通过题数为关键字分裂出同一题数的区间,再按照罚时为关键字分裂出罚时较少的区间,此时答案为比他通过题数多的人数加上同一题数罚时较少的人人数,然后插入即可。

posted @ 2022-10-11 18:56  Broken_Eclipse  阅读(68)  评论(0编辑  收藏  举报

Loading