头图

简单写一写,以免忘记


FHQ-treap是一种神奇的数据结构,支持很多操作。包括但不限于查排名,删数,查找前驱后继

首先我们来学习FHQ-treap的前置知识:二叉搜索树(BST)

二叉搜索树

二叉搜索树显然是一棵二叉树,但这个二叉树有特殊性质:他的左子树中所有点权值均小于他,他的右子树中所有点的权值都大于他(大部分时候,二叉搜索树一个节点中会存储若干个一样权值的数)

我们可以发现,二叉搜索树本身就可以支持很多操作。之后FHQ-treap的很多操作也和二叉搜索树的性质息息相关。这样的操作理想情况下一次只需要花费 $O(logn)$ 的时间

但二叉搜索树极易脱离理想状态。最简单的状况,将数从小到大依次插入,这时树会退化成一条链。

为了克服这个确定,前辈们发明了众多算法。较为知名的有splay,treap,替罪羊树,fhq-treap。

(博主曾经写过一篇splay的总结,但她神秘地消失在了博主的电脑里)

所以我们来学习FHQ-treap

FHQ-treap

FHQ是一位大佬的名字,treap是一种数据结构

根据题目我们就知道,这个数据结构是FHQ大佬对treap的魔改。

先来了解一下treap的核心思想:treap给每个节点都赋了一个额外的随机权值,使这棵树在原有权值满足二叉搜索树的性质的同时,新增权值满足堆的性质(小根堆大根堆无所谓,反正值是随机赋的。但我们这里采用小根堆)

看起来这似乎很不靠谱,因为跟随机化有关。那么我来告诉你一个方法:打开明日方舟,往池子里扔两个十连(一个十连会保五星),如果第二个十连不是十个三星的话

你就可以继续学下去了

讲真,这个东西被卡的概率实在太小了。要所有随机的值都达到一种精妙的平衡,使树的深度比logn大很多。这时数据还要正好卡在这一条长链上,出题人还不能故意卡你,因为你赋的值是随机的

当然,卡treap的方法还是有的,只要出题人献祭双亲造出能使treap的稍大常数与splay区分开来的数据,那么就把treap卡掉了。但双亲还是很宝贵的,所以没多少人这么做

treap有什么问题呢?

当然是有的,最大的一个问题就是难写。左右旋分分钟让人丧失对生活的信心,虽然说很模块化但理解起来也很费劲啊。而且treap和splay的一个大区别是splay的旋转包含在splay操作中了,实际上单独使用旋转的操作并不多。但treap又不一样

那么FHQ大佬就不乐意了,于是造出来了FHQ-treap。

FHQ-treap用分裂与合并两个小清新操作代替了繁琐的旋转。我们来一次看看:

分裂操作

常见的分裂操作有两种,按权值分裂与按排名分裂。我们先讲按权值分裂

我们传进去原来的根t,与用于分裂的权值k。我们现在想要小于等于k的权值放一棵树里,大于k的放另一棵树里。

现在我们到了一个节点p,那么我们分类讨论一下

  1. val[p] <= k

这种时候p连同她的左子树肯定都小于等于k,都要归到小于等于k的树里。

那么她的右子树呢?我们是不知道的。所以要向右子树遍历。左子树就不用管了

那么我们也可以推出p在新树里的位置了。她是之前节点的右子树的一部分,这就意味着p此时权值为这棵树中的最大值。直接放到最右边的儿子上就行了。

  1. val[p] > k

em...这个就不用讲了吧,把前几行左右互换,最大值变成最小值就行了

在构造新树时,我们有一个小寄巧:把函数设成这个样子split(int now, int k, int &x, int &y),这样如果我们使用split(son[now]1, k, sonnow, y)的话,sonnow的值是会随x变化的。

然后我们可以构造出以下代码:


void split(int now, int k, int &x, int &y)
{
    if(now == 0)
    {
        x = y = 0;
        return ;
    }
    if(val[now] <= k)
    {
        x = now;//这样子就把now新树中的父亲的右儿子设为了now
        split(s[now][1], k, s[now][1], y);//为什么这里变成了s[now][1]了呢?因为这时now已经是新树中的一个节点了,她的左子树会跟着她一起到新树中。此时新树中now的右儿子还不能确定是哪个节点。所以在下一次有节点加入新树时执行上一行的操作就行了
        //其实我觉得先看第二行再看第一行好理解些。。。
    }
    else
    {
        y = now;    
        split(s[now][0], k, x, s[now][0]);
    }
    push_up(now);
}

用了这个技巧,在构造新树时左右儿子的设置就变简单许多了~

排名操作与之类似,将k与左儿子子树节点个数相比,只不过如果k大于左子树的节点个数就要减去个数再减1(减去自己)再遍历右子树

void split(int now, int k, int &x, int &y)
{
    if(now == 0)    
    {
        x = y = 0;
        return ;
    }
    if(lt[now] == 1) push_down(now);
    if(si[s[now][0]] < k)
    {
        x = now;
        split(s[now][1], k - si[s[now][0]] - 1, s[now][1], y);
    }   
    else
    {
        y = now;
        split(s[now][0], k, x, s[now][0]);
    }
    push_up(now);
}

合并

我们既然要合并,首先就要先分裂。这样产生的两个新树,其中一个树节点的最大值必定小于另一个树节点的最小值。设小树中合并到的节点为x,大树中合并到的节点为y.显然,根据随机的权值大小,一共只有两种情况,一种合并后y在x的右子树中,一种合并后x在y的左子树中

为什么我不说y是x的右儿子呢?万一x原来的右儿子也小于y就不满足堆的性质了(假设我们写的是小根堆)

所以我们还要把y与原来的x的右儿子作比较,然后返回现在x的右儿子

代码很简单:

int merge(int x, int y)
{
    if(x * y == 0) return x + y;//当只有x,y至少有一个为0时,说明只有一个子树。直接返回那个子树的根节点(如果都为0则返回0,不影响)
    if(num[x] > num[y])//num为随机权值
    {
        s[x][1] = merge(s[x][1], y);
        push_up(x);
        return x;
    }
    else
    {
        s[y][0] = merge(x, s[y][0]);
        push_up(y);
        return y;
    }
}

讲完这两个操作,就能开始倒腾平衡树操作了~

插入

设插入的数权值为k

把原树按k分裂,然后新建一个节点,把这三东西合并就行了

删除

设要删除的数为k,

把原树按k - 1分裂,再把k~max的那颗树按k分裂。这时我们得到了一颗权值全为k的树。把这棵树的左右儿子合并,根节点丢掉不管,然后再把这三东西合一起就行了

依数查排名

设给的数为k

把原树按k - 1分裂,k的排名即为小的树的大小+1

记得合并回去

依排名查数

设给的排名为k,

从根节点开始遍历,设当前节点为u,如果k等于u的左子树大小+1,那么说明k代表的就是根节点

如果小于,直接跳到左儿子

如果大于,将u -= (左子树大小+1)再跳到右儿子

很好理解

查前驱

设给的数为k

按u - 1分裂,找小的树的最大值(一直往右儿子跳,直到没有右儿子)

查后继

设给的数为k

按u分裂,找大的树的最小值(一直往右儿子跳,直到没有右儿子)

注意,以上操作,一旦合并,根就要更新

例题:P3369 普通平衡树

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int root;
int cnt, val[maxn], s[maxn][2], num[maxn], si[maxn];//val为题目给的权值,num为随机取的权值
void push_up(int u)
{
    si[u] = 1;
    if(s[u][0]) si[u] += si[s[u][0]]; 
    if(s[u][1]) si[u] += si[s[u][1]]; 
}
void split(int now, int k, int &x, int &y)
{
    if(now == 0)
    {
        x = y = 0;
        return ;
    }
    if(val[now] <= k)
    {
        x = now;
        split(s[now][1], k, s[now][1], y);
    }
    else
    {
        y = now;    
        split(s[now][0], k, x, s[now][0]);
    }
    push_up(now);
}
int merge(int x, int y)
{
    if(x * y == 0) return x + y;
    if(num[x] > num[y])
    {
        s[x][1] = merge(s[x][1], y);
        push_up(x);
        return x;
    }
    else
    {
        s[y][0] = merge(x, s[y][0]);
        push_up(y);
        return y;
    }
}
int make_new(int v)
{
    val[++ cnt] = v;
    num[cnt] = rand();
    si[cnt] = 1;
    return cnt;
}
void ins(int u)
{
    int x = 0, y = 0;
    split(root, u - 1, x, y); 
    root = merge(merge(x, make_new(u)), y);
}
void del(int u)
{
    int x = 0, y = 0, z = 0;
    split(root, u - 1, x, y);
    split(y, u, y, z);
    y = merge(s[y][0], s[y][1]);
    root = merge(x, merge(y, z));
}
int find_rank(int u)
{
    int x = 0, y = 0;
    split(root, u - 1, x, y);
    int ans = si[x] + 1;
    root = merge(x, y);
    return ans;
}

int find_num(int x)
{
    int u = root;
    while(1)
    {
        if(si[s[u][0]] + 1 == x) break;
        if(si[s[u][0]] + 1 > x) u = s[u][0];
        else x -= (si[s[u][0]] + 1), u = s[u][1];
    }
    return val[u];
}
int find_res(int u)
{
    int x = 0, y = 0;
    split(root, u - 1, x, y);
    int ans = x;
    while(s[ans][1]) ans = s[ans][1];
    ans = val[ans];
    root = merge(x, y);
    return ans;
}
int find_pre(int u)
{
    int x = 0, y = 0;
    split(root, u, x, y);
    int ans = y;
    while(s[ans][0]) ans = s[ans][0];
    ans = val[ans];
    root = merge(x, y);
    return ans;
}
int main(){
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    int t;
    cin >> t;
    int opt, x;
    while(t --)
    {
        cin >> opt >> x;
        if(opt == 1) ins(x);
        if(opt == 2) del(x);
        if(opt == 3) cout << find_rank(x) << endl;
        if(opt == 4) cout << find_num(x) << endl;
        if(opt == 5) cout << find_res(x) << endl;
        if(opt == 6) cout << find_pre(x) << endl;

    }
    return 0;
}

fhq-treap的区间操作

典型例题:P3391 文艺平衡树

设要操作的区间为l~r

没有权值了,我们按排名分裂。将1~(l - 1)项,l~r项,r + 1 ~ n项分开,给l~r的根节点打上类似于线段树的懒标记即可。输出就是中序输出

懒标记什么时候下传呢?当我们需要用到儿子信息时就要下传了。所以分裂,合并,输出时都要下传一下

分裂时k减去的数一定要+1.警钟撅烂

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;
int root;
int cnt, val[maxn], s[maxn][2], num[maxn], si[maxn], lt[maxn];
void push_up(int u)
{
    si[u] = 1;
    if(s[u][0]) si[u] += si[s[u][0]]; 
    if(s[u][1]) si[u] += si[s[u][1]]; 
}
void push_down(int u)
{
    // cout << u << endl;
    swap(s[u][0], s[u][1]);
    if(s[u][0]) lt[s[u][0]] ^= 1;
    if(s[u][1]) lt[s[u][1]] ^= 1;
    lt[u] = 0;
}
void split(int now, int k, int &x, int &y)
{
    if(now == 0)    
    {
        x = y = 0;
        return ;
    }
    if(lt[now] == 1) push_down(now);
    if(si[s[now][0]] < k)
    {
        x = now;
        split(s[now][1], k - si[s[now][0]] - 1, s[now][1], y);
    }   
    else
    {
        y = now;
        split(s[now][0], k, x, s[now][0]);
    }
    push_up(now);
}
int merge(int x, int y)
{
    if(x * y == 0) return x + y;
    if(num[x] > num[y])
    {
        if(lt[x]) push_down(x);
        s[x][1] = merge(s[x][1], y);
        push_up(x);
        return x;
    }
    else
    {
        if(lt[y]) push_down(y);
        s[y][0] = merge(x, s[y][0]);
        push_up(y);
        return y;
    }
}
int make_new(int v)
{
    val[++ cnt] = v;
    num[cnt] = rand();
    si[cnt] = 1;
    return cnt;
}
void ins(int u)
{
    root = merge(root, make_new(u));
}
void p(int u)
{
    if(lt[u] == 1) push_down(u);
    if(s[u][0]) p(s[u][0]);
    cout << val[u] << ' ';
    if(s[u][1]) p(s[u][1]);
}
int main(){
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    int n, m, l, r;
    cin >> n >> m;
    for(int i = 1;i <= n;i ++) ins(i);
    while(m --)
    {
        cin >> l >> r;
        int x, y, z;
        split(root, l - 1, x, y);
        split(y, r - l + 1, y, z);
        lt[y] ^= 1;
        root = merge(x, merge(y, z));
    }
    p(root);
    return 0;
}

典型例题讲解

懒得写了,看心情补

最后的话

明日方舟里三星的出率是比四星还低的,如果你在bilibili上搜抽卡视频, 出3个六星的都比出10个三星的视频多。实际上博主只看到过一个10三星的视频


田井中葎
1 声望0 粉丝

引用和评论

0 条评论