非旋平衡树(fhq-treap)详解

主要参考:FHQ-Treap(非旋treap/平衡树)——从入门到入坟_hsez_yyh的博客-CSDN博客_fhq树

平衡树这玩意吗,首先他的是一棵树对吧,而且得是一颗二叉树

肯定还得是个二叉搜索树吧,

于是我们可以定义如下结构体:

struct node{
    int l,r,key,val,si;
}tr[200010];

其中si表示整个以此节点为根的子树的大小,val表示优先级。key就是此节点所带的键值

平衡树就是建立在键值与优先级的基础上的。

顾名思义,平衡树,肯定得让这棵树左右两边平衡而不是差距过大,来保持logn级的复杂度,优先级就是这个目的

这时我们需要引出一个重要的性质:

对于任意fhq-treap中的节点 i ,其左子树上的所有节点的key小于等于 i 节点的key值,i 节点所有右子树上所有节点的key 大于等于 i 节点的key值;对于任意节点 i ,其左、右儿子的val值大于等于 i 的val值

嗯,似曾相识。

没错,与小根堆一样,不然你以为treap这名字怎么来的?(treap = tree+heap)

但在这里还是提醒一句:千万别记错了,不然直接爆零(应该没人会比我傻吧)

fhq-treap相较于splay,有着一个不会退化成链好处,还有一个具有争议的部分,时间复杂度,究竟是o(nlogn)还是O(nlog^2(n))

我是不知道了,还请读者自行判断(偷懒了)个人偏向第一种(主要是我算不来啊)

接着讲第二点

分裂与合并操作,主要作用就是代替旋转:

分裂,一看就知道是将树扯成两半,只不过是按照一个参照来分裂

众所周知,二叉搜索树所有的左子树小于根而右子树大于根

根据这一点,我们即可以把树分为小于参照的与大于参照的

下面代码中的x即为参照,p为当前位置l和r即是指向左右子树

复制代码
void split(int p,int x,int &l,int &r){
   //p为当前所在节点,l,r,为分裂后的值,这里用指针是方便传递值(应该不会有人认为这值就不用了吧)
    if(!p){//判断此树是否为空
        l = r = 0;
        return ;
    }
    if(tr[p].key<=x){//当参照大于等于当前节点键值,左子树全部小于当前节点键值,所以全部小于参照
        l = p;//当前节点为其左子树
        split(tr[l].r,x,tr[l].r,r);//继续往右子树
    }else{//当参照小于当前节点键值,右子树值全部大于键值
        r = p;//当前节点为右子树
        split(tr[r].l,x,l,tr[r].l);//继续往左子树找
    }
    pushup(p);//与线段树一个道理,向上更新
}
复制代码

合并吗,就是把分出来的两半再合一起呗

当然,这个就没有参照了,反正就比个大小然后就并起来了

复制代码
int merge(int l,int r){
    if(!l||!r)//如果有空树
        return l|r;//返回空树
    if(tr[l].val<=tr[r].val){
     //左子树优先级小于右子树,合并左右子树
        tr[l].r = merge(tr[l].r,r);
        pushup(l);//别忘了更新
        return l;
    }else{
     //同理
        tr[r].l = merge(l,tr[r].l);
        pushup(r);
        return r;
    }
}
复制代码

第三点:

插入与删除

插入吗,也就是建树,我们只需要给这个需要插入的元素找个合适的位置就行了

原来嘛自然是用旋转,但我们现在有了分裂与合并操作

插入自然是先分裂,给x找出位置后再合并

删除就是将树分裂成三部分,再将除了x的两部分合并起来,还需要记录一下根节点

复制代码
void insert(int x){
    split(rt,x-1,dl,dr);
    rt = merge(merge(dl,getrand(x)),dr);
}
void delet(int x){
    split(rt,x-1,dl,dr);
    split(dr,x,tmp,dr);
    tmp = merge(tr[tmp].l,tr[tmp].r);
    rt = merge(dl,merge(tmp,dr));
}
复制代码

第四点:
查询x的排名,这个应该不用多说

int getrk(int x){
    split(rt,x-1,dl,dr);
    int rnk = tr[dl].si+1;
    rt = merge(dl,dr);
    return rnk;
}

第五点:

查询前驱与后继,查询排名为x的数

此处思想就为二叉搜索树

若要查前驱,getnum(dl,tr[dl].si);因为前缀是<=x中排名最后的

若要查后继,getnum(dr,1);因为后缀即为>x中排名第一的

查询排名为x的数就是直接getnum(root,x);

读者可自己揣摩揣摩

int getnum(int p,int x){
    int u = tr[tr[p].l].si+1;
    if(u==x) return tr[p].key;
    if(u>x) return getnum(tr[p].l,x);
    else return getnum(tr[p].r,x-u);
}

于是乎,将这些结合在一起,我们就能A了这道板子题题目链接

最后说一句,多多手动模拟,会对其理解更深(不是我想整你们·,真不是)

此处为维护值域的平衡树,过几天再补上一个维护序列的

蒟蒻瞎扯完毕

2022-10-28 16:44:43


更新一手

fhq-treap维护区间修改

此时,split函数需要重新写,void split(int p,int x,int &l,int &r)的意思不再是分离<=k和>k的两棵树

而是变为了 将序列p的前x个元素分离出来形成一棵新fhq-treap的l子树,另外的一部分形成另一颗 fhq-treap的r 子树

所以,split你会发现写法像极了前面的普通平衡树的getnum函数

复制代码
void split(int p,int x,int &l,int &r){
    if(!p){
        l = r = 0;
        return ;
    }
    pushdown(p);
    int u = tr[tr[p].l].si+1;
    if(u<=x){
        l = p;
        split(tr[l].r,x-u,tr[l].r,r);
        pushup(l);
    }else{
        r = p;
        split(tr[r].l,x,l,tr[r].l);
        pushup(r);
    }
}
复制代码

完整版例题+代码可以看这里

posted @   cztq  阅读(170)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】

阅读目录(Content)

此页目录为空

点击右上角即可分享
微信分享提示