浅析FHQ-treap

fhq-treap 又名“无旋 treap”,有着码量小,易理解,可持久化等特点。

luogu 阅读链接

前言

fhq-treap 又名“无旋 treap”,有着码量小,易理解,可持久化等特点。
但 fhq-treap 的常数较大。
必要:

选要:

基础操作

treap 的每个节点都有一个随机的优先级
treap 的权值要具有二叉搜索树的性质优先级满足堆的性质
fhq-treap 有两个关键函数 sliptmerge,分别是分裂合并
代码中均为小根堆。

节点

在 fhq-treap 中我们需要维护子树大小,节点权值,左右儿子,优先级(随机数,用于堆)。

std::mt19937 rd(std::chrono::steady_clock::now().time_since_epoch().count());
//需要 <random> 和 <chrono> 头文件,不要放在结构体内
struct node{
    int size, val, rank, ls, rs;
}d[N];
int tot = 0, root = 0;
int newnode(int val){
    d[++tot].val = val, d[tot].rank = rd();
    d[tot].size = 1, d[tot].ls = d[tot].rs = 0;
    return tot;
}
void getsize(int u){d[u].size = 1 + d[d[u].ls].size + d[d[u].rs].size;}

分裂

按权值分裂

图
明显,我们会把一个树,把权值 k 的放在 L 树中,>k 的放在 R 树中。

void SplitVal(int now, int val, int&L, int&R){
    if(!now)return void(L = R = 0);
    if(d[now].val <= val)L = now, SplitVal(d[now].rs, val, d[now].rs, R);
    else R = now, SplitVal(d[now].ls, val, L, d[now].ls);
    return getsize(now);
}

我们理解一下代码。
我们现在遍历到了原树的 now 节点。
如果节点是空的那么 L 和 R 树都是空的。

如果这个节点的权值 k
     那么我们把他放在 L 树中,然后行对 now 节点的右儿子遍历,由于是二叉搜索树,所以 now 的左儿子的权值均 k,所以只用考虑分割 now 的右儿子的是在 L 树的右儿子,还是在 R 树内。

反之同理。

按排名分裂

同理,只不过要判断左儿子的大小关系。

void SplitRank(int now, int k, int&L, int&R){
    if(!now)return void(L = R = 0);
    int size = d[d[now].ls].size;
    if(k <= size)R = now, SplitRank(d[now].ls, k, L, d[R].ls);
    else L = now, SplitRank(d[L].rs, k - size - 1, d[L].rs, R);
    getsize(now);
}

合并

合并是分裂的逆操作,我们回到之前的这幅图。
图
如果要合并,我们需要保证 xL,yR,val(x)val(y)
由于两棵树有序,只需要根据优先级考虑哪颗树放“上面”,哪棵树放“下面”,即考虑哪棵树成为子树。
同时,我们还需要满足二叉搜索树的性质。
所以若 L 的根结点的优先级 <R 的,则 L 成为根结点,由于 R 的权值 L,所以 RL 的右子树合并;
反之,则 R 作为根结点,LR 的左子树合并。

int merge(int L, int R){
    if(!L || !R)return L + R;
    if(d[L].rank < d[R].rank){
        d[L].rs = merge(d[L].rs, R), getsize(L);
        return L;
    }
    else {
        d[R].ls = merge(L, d[R].ls), getsize(R);
        return R;
    }
}

其他

有了分裂和合并,剩下的函数就很好实现了。

插入

新建的节点的权值的 val,我们将 val>val 的分裂,然后将新建的节点合并。

void insert(int val){
    int L, R;
    SplitVal(root, val, L, R);
    root = merge(merge(L, newnode(val)), R);
}

这样我们需要调用一次分裂和两次合并,常数明显很大。
我们可以优化:

void insert(int val){
    int*u = &root, z = newnode(val), r = d[z].rank;
    for(;*u && (d[*u].rank < r);u = &(val < d[*u].val ? d[*u].ls : d[*u].rs))
        ++d[*u].size;
    SplitVal(*u, val, d[z].ls, d[z].rs), *u = z, getsize(z);
}

我们可以用循环寻找我们要插入的位置,然后把路径上的点的子树大小增加。
接着将这个位置原本的树按权值分裂成两棵,然后放在插入节点的两个儿子。

删除

我们将 <val=val>val,的部分分裂出来,最中间的那棵树就是要删除的。
由于只要删除一个数,我们将中间部分的左右儿子合并,然后将剩余的部分合并。

void del(int val){
    int L, mid, R;
    SplitVal(root, val, L, R);
    SplitVal(L, val - 1, L, mid);
    mid = merge(d[mid].ls, d[mid].rs);
    root = merge(merge(L, mid), R);
}

我们用相似的方式进行优化。
因为题目保证删除的点存在(不保证就找到后再扫一遍),我们直接将路径上的子树大小修改。
然后将删除点的两个子树拼起来,放在删除点的原本位置。

void del(int val){
    int*u =&root;
    for(;*u && d[*u].val != val;u = &(val < d[*u].val ? d[*u].ls : d[*u].rs))--d[*u].size;
    if(u) *u = merge(d[*u].ls, d[*u].rs);
}

查询部分

可以像开头 BST 的那样询问,也可以像这里借助分裂合并(常数较大):

//我们将 < x 的分裂出,然后一直往右走,走到头就是前驱。
int pre_val(int x){
    int L, R, now;
    SplitVal(root, x - 1, L, R), now = L;
    while(d[now].rs)now = d[now].rs;
    return root = merge(L, R), d[now].val;
}
//类似
int next_val(int x){
    int L, R, now;
    SplitVal(root, x, L, R), now = R; 
    while(d[now].ls)now = d[now].ls;
    return root = merge(L, R), d[now].val;
}
//将 < x 的分裂,答案就是这棵树的大小。
int query_rank(int val){
    int L, R;
    SplitVal(root, val - 1, L, R);
    int ans = d[L].size + 1;
    return root = merge(L, R), ans;
} 
//和 BST 中一样
int kth(int k, int rak){
    while(1){
        if (rak <= d[d[k].ls].size)k = d[k].ls;
        else if (!(rak -= d[d[k].ls].size + 1))return d[k].val;
        else k = d[k].rs;
    }
}

完整代码

P3369 普通平衡树
P3369 插入删除优化
P6136 普通平衡树加强版

序列操作

我们可以将树的中序遍历看做序列顺序的。
然后用按排名分裂,分成 [1,l1],[l,r],[r+1,n] 三块。
给中间的块打上标记,之后在访问的时候 pushdown 即可。

例题:

P3391 文艺平衡树代码
P2042 维护序列

可持久化

不知道什么是可持久化的看这:可持久化数据结构简介
打上注释的是添加的操作。

void split(int now, int val, int&L, int&R){
    if(!now)return void(L = R = 0);
    int w;
    if(d[now].val <= val){
        d[L = newnode(1)] = d[now];//
        split(d[now].rs, val, d[L].rs, R), getsize(L);
    }
    else {
        d[R = newnode(1)] = d[now];//
        split(d[now].ls, val, L, d[R].ls), getsize(R);
    }
    return getsize(L);
}
int merge(int L, int R){
    if(!L || !R)return L + R;
    int w;
    if(d[L].rank < d[R].rank){
        d[w = newnode(1)] = d[L];//
        return d[w].rs = merge(d[w].rs, R), getsize(w), w;
    }
    else {
        d[w = newnode(1)] = d[R];//
        return d[w].ls = merge(L, d[w].ls), getsize(w), w;
    }
}

记得用一个数组存一下每个版本的根。

例题

P3835 可持久化平衡树

模版,直接往上套。
{% folding blue::代码 %}
由于借鉴了题解,我又懒得重写,所以这里的码风不好
洛谷云剪贴板

P5055 可持久化文艺平衡树

打个标记就好了。
洛谷云剪贴板

P5350 序列

挺烦的一道题,要定期重构,遍历完后再清空节点数,pushdown 也有新建节点
洛谷云剪贴板

posted @   fush's_blog  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示