3

FHQ Treap 详解 - Jerry_Jiang

 1 year ago
source link: https://www.cnblogs.com/Jerry-Jiang/p/FHQ_Treap.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

一些鲜花放在前面,平衡树学了很久,但是每学一遍都忘,原因就在于我只能 70% 理解 + 30% 背板子,所以每次都忘。这次我采取了截然不同的策略,自己按照自己的理解打一遍,大获成功(?),大概打 20 min,调 10 min 结束,然后写下了这篇文章。

虽然但是,感觉 Treap 还是很强的,代码好写好调,而且可以解决很多问题(下面将会提到),好像说是常数大一点?无伤大雅吧……

Treap

首先,从 Treap 的定义开始。Treap 实际上是一种笛卡尔树(笛卡尔树可以看 这篇文章,每个点有两个信息 (vu,pu)(vu,pu),分别表示点 uu 的权值与优先度。vuvu 形成一个二叉搜索树(左儿子的值 ≤≤ 自己的值 ≤≤ 右儿子的值),pupu 形成一个大根堆(自己的值 ≥≥ 左右儿子的值)。

你可以利用 Treap 做很多有用的操作,但是 Treap 有一个缺点,就是如果被卡成链怎么办?

FHQ Treap

这时候就需要用到 FHQ Treap,它基于 Treap 的基础上对每个点的 pp 值都进行了随机化操作,这样就可以达到平衡的目的了。为什么?因为随机出来不平衡的概率很小,大多随机数都是无规律的,这样通过大根堆的性质就可以使它达到平衡,这样树高期望就是 O(logn)O(log⁡n) 层的了,并且无法卡掉,因为随机数是程序生成的,并非数据所决定。

系统随机函数若以 srand(time(0)) 为随机种子,效果不佳,推荐使用 mt19937 rnd(233),生成随机数更加均匀。

FHQ Treap 又叫无旋 Treap,它只需要两个核心操作 merge()split() 即可完成所有的复杂操作,就像玩拼图一样把一棵树拆开再拼起来一样。下面就来具体介绍一下这两个函数到底是如何实现的。

关于 upd 函数的说明

(这个可以学完下面的再看)upd 函数,即刷新一个节点大小的函数。在 split() 中,在分裂完子树后最后应该刷新,不然可能大小信息已经被更改而不知道。同理,在 merge() 中,合并子树后也应当刷新,调不出来一定要检查一下是否因忘记刷新而错误。

split 函数

split 函数实现的功能是把一棵树按照权值大小拆分成两棵树,具体来说,split(int cur,int k,int &x,int &y) 表示将以 cur 为根的子树拆分成两棵树 xx 和 yy,其中 xx 里的权值都 ≤k≤k,yy 里的权值都 >k>k。

怎么写呢?首先,为了方便返回,直接将要拆分成的两棵子树引用定义在函数参数中。先特判一下,如果要拆分的树为空,则拆分出来的两棵树也为空。不然就判断树根是否 ≤k≤k,如果是,则树左子树都 ≤k≤k,即属于 xx,那么只要分裂右子树即可,反之亦然。

代码

cpp
void split(int cur,int k,int &x,int &y)
{
    if(!cur)
    {
        x=y=0;
        return;
    }
    if(tree[cur].val<=k)
    {
        x=cur;
        split(tree[x].rs,k,tree[x].rs,y);
    }
    else
    {
        y=cur;
        split(tree[y].ls,k,x,tree[y].ls);
    }
    upd(cur);
}

merge 函数

merge 函数就是把两个树 x,yx,y 合并,保证所有 xx 中的 vv 都 ≤≤ 所有 yy 中的 vv,具体来说,就是 int merge(int x,int y)

写法就是判断 x,yx,y 中是否有空树,有的话直接返回另一个即可。不然比较两者根的有先值,如果第一个大,根据大根堆性质,显然应该合并 xx 的右子树和 yy,反之亦然。

代码

cpp
int merge(int x,int y)
{
    if(!x||!y)
        return x+y;
    if(tree[x].key>=tree[y].key)
    {
        tree[x].rs=merge(tree[x].rs,y);
        upd(x);
        return x;
    }
    else
    {
        tree[y].ls=merge(x,tree[y].ls);
        upd(y);
        return y;
    }
}

其他操作的实现

下面来看看 FHQ Treap 能实现哪些操作吧,请看模板题 普通平衡树。(下面全部内容为笔者自己发挥,若有更好做法请在评论区留言或私信笔者)

插入 xx:将根以 xx 值分裂,在中间建一个新的点合并回去即可,比较容易。
删除 xx:将根分别以 x−1,xx−1,x 分裂,中间一个树就是权值为 xx 的树,把这个树替换为它左右子树合并的结果即可(因为只要删一个,相当于把根节点给删了。
查询 xx 的排名:这个非常容易,直接按 x−1x−1 分裂并输出第一个子树大小 +1 即可。
查询排名为 xx 的数:这个可以从根节点开始,不停地判断应该往左子树走还是右子树走,判断方式就是看当前点地左子树大小 +1 和 xx 的大小关系,若相等则直接退出。
查询 xx 的前驱:笔者做法比较暴力,直接将数按 x−1x−1 分裂,第一棵树的最后一个就是,最后一个求可以查询排名为数大小的数,用之前的函数可以求出。
查询 xx 的后继:同前驱做法,按 xx 分裂,第二棵树的第一个就是,同样查询排名为 1 的树即可,使用之前的函数。

具体的还是看代码吧。

代码

cpp
#include <bits/stdc++.h>
#define TIME 1e3*clock()/CLOCKS_PER_SEC
using namespace std;
// stay organized
mt19937 rnd(233);
const int maxn=1e5+10;
struct Node
{
    int ls,rs;
    int siz;
    int val,key;
}tree[maxn];
int rt=0,tot=0;
int newnode(int val)
{
    tot++;
    tree[tot].ls=tree[tot].rs=0;
    tree[tot].siz=1;
    tree[tot].val=val;
    tree[tot].key=rnd();
    return tot;
}
void upd(int x)
{
    tree[x].siz=tree[tree[x].ls].siz+1+tree[tree[x].rs].siz;
}
void split(int cur,int k,int &x,int &y)
{
    if(!cur)
    {
        x=y=0;
        return;
    }
    if(tree[cur].val<=k)
    {
        x=cur;
        split(tree[x].rs,k,tree[x].rs,y);
    }
    else
    {
        y=cur;
        split(tree[y].ls,k,x,tree[y].ls);
    }
    upd(cur);
}
int merge(int x,int y)
{
    if(!x||!y)
        return x+y;
    if(tree[x].key>=tree[y].key)
    {
        tree[x].rs=merge(tree[x].rs,y);
        upd(x);
        return x;
    }
    else
    {
        tree[y].ls=merge(x,tree[y].ls);
        upd(y);
        return y;
    }
}
int x,y,z;
void ins(int val)
{
    split(rt,val,x,y);
    rt=merge(merge(x,newnode(val)),y);
}
void del(int val)
{
    split(rt,val-1,x,y);
    split(y,val,y,z);
    y=merge(tree[y].ls,tree[y].rs);
    rt=merge(merge(x,y),z);
}
int rnk(int val)
{
    split(rt,val-1,x,y);
    int ans=tree[x].siz+1;
    rt=merge(x,y);
    return ans;
}
int kth(int now,int k)
{
    while(tree[tree[now].ls].siz+1!=k)
    {
        if(tree[tree[now].ls].siz+1>k)
            now=tree[now].ls;
        else
        {
            k-=tree[tree[now].ls].siz+1;
            now=tree[now].rs;
        }
    }
    return tree[now].val;
}
int pre(int val)
{
    split(rt,val-1,x,y);
    int ans=kth(x,tree[x].siz);
    rt=merge(x,y);
    return ans;
}
int suf(int val)
{
    split(rt,val,x,y);
    int ans=kth(y,1);
    rt=merge(x,y);
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin>>t;
    int opt,x;
    while(t--)
    {
        cin>>opt>>x;
        if(opt==1)
            ins(x);
        else if(opt==2)
            del(x);
        else if(opt==3)
            cout<<rnk(x)<<'\n';
        else if(opt==4)
            cout<<kth(rt,x)<<'\n';
        else if(opt==5)
            cout<<pre(x)<<'\n';
        else cout<<suf(x)<<'\n';
    }
    return 0;
    // you should actually read the stuff at the bottom
}

/* stuff you should look for
    * int overflow, array bounds
    * clear the arrays?
    * special cases (n=1?),
    * WRITE STUFF DOWN,
    * DON'T GET STUCK ON ONE APPROACH
*/

完结撒花 owo~

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK