AcWing 253:普通平衡树 ← FHQ-Treap 树(结构体 + split + merge)

【题目来源】
https://www.luogu.com.cn/problem/P3369
https://www.acwing.com/problem/content/255/

【题目描述】
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1. 插入数值 x。
2. 删除数值 x(若有多个相同的数,应只删除一个)。
3. 查询数值 x 的排名(若有多个相同的数,应输出最小的排名)。
4. 查询排名为 x 的数值。
5. 求数值 x 的前驱(前驱定义为小于 x 的最大的数)。
6. 求数值 x 的后继(后继定义为大于 x 的最小的数)。
注意: 数据保证查询的结果一定存在。

【输入格式】
第一行为 n,表示操作的个数。
接下来 n 行每行有两个数 opt 和 x,opt 表示操作的序号(1≤opt≤6)。

【输出格式】
对于操作 3,4,5,6 每行输出一个数,表示对应答案。

【数据范围】
1≤n≤100000,所有数均在 −10^7 到 10^7 内。

【输入样例】
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

【输出样例】
106465
84185
492737

【算法分析】
(一)FHQ−Treap 简介
● FHQ−Treap,也称非旋 Treap,由范浩强提出。顾名思义,FHQ−Treap 就是不需要通过旋转,而是通过分裂(split)与合并(merge)维护的 Treap。FHQ−Treap 与 Treap 的另外一个区别是 FHQ−Treap 可持久化。
● FHQ-Treap 的高明之处在于所有的操作都只用到了分裂(split)与合并(merge)这两个基本操作。
● 本题的 Treap 树实现参见:https://blog.csdn.net/hnjzsyjyj/article/details/138482439
● 本题的替罪羊树实现参见:https://blog.csdn.net/hnjzsyjyj/article/details/128647972

(二)核心代码解析
● pushup:更新子树大小

void pushup(int p) {
    // 子树大小 = 左子树大小 + 右子树大小 + 自身(1)
    tr[p].size=tr[tr[p].le].size+tr[tr[p].ri].size+1;
}

(1)作用:当节点的左右子树发生变化(如分裂、合并、插入)时,更新当前节点的 size;
(2)为什么需要:size 是实现 “查排名、查第 k 小” 的核心依据(比如左子树大小就是 “比当前节点小的节点数”);
(3)调用时机:分裂(split)、合并(merge)操作后,必须调用 pushup 保证 size 正确。

● get_node:创建新节点

int get_node(int val) {
    tr[++idx].val=val;   // 分配新节点编号,赋值数值
    tr[idx].pri=rand();  // 随机生成优先级(保证堆性质)
    tr[idx].size=1;      // 新节点的子树大小初始为1(只有自己)
    return idx;          // 返回新节点的编号
}

(1)作用:封装 “创建节点” 的逻辑,避免重复代码;
(2)随机 pri 的关键:通过随机数让树的结构随机化,从而保证 Treap 的平衡(平均高度为 
O(logn))。

●​​​​​​​ split:按值分裂 Treap

void split(int u,int v,int &x,int &y) {
    // u:当前要分裂的子树根节点;v:分裂阈值;x/y:分裂后的左右子树(引用传递)
    // 分裂规则:x 存储所有 val <= v 的节点,y 存储所有 val > v 的节点
    if(!u) x=y=0;  // 空节点:分裂结果都是空
    else {
        if(tr[u].val<=v) {
            // 当前节点属于 x,分裂其右子树(右子树可能有 >v 的节点)
            x=u;
            split(tr[u].ri,v,tr[u].ri,y);
        } else {
            // 当前节点属于 y,分裂其左子树(左子树可能有 <=v 的节点)
            y=u;
            split(tr[u].le,v,x,tr[u].le);
        }
        pushup(u);  // 分裂后更新当前节点的 size
    }
}

(1)核心逻辑:递归按 BST 性质拆分树,把 “<=v” 的节点归到 x,“>v” 的归到 y;
(2)引用参数 &x,&y:因为要修改调用方的变量(比如 insert 里的 x/y),必须用引用;
(3)举例:若树中有 [84185, 89851, 106465],以 v=100000 分裂,x 是 [84185,89851],y 是 [106465]。

● merge:合并两个 Treap

int merge(int x,int y) {
    // 合并规则:x 中所有节点 val <= y 中所有节点 val(必须满足,否则破坏 BST)
    // 返回值:合并后的新根节点编号
    if(!x || !y) return x+y;  // 空树直接返回另一棵树(0+非0=非0,0+0=0)
    if(tr[x].pri>tr[y].pri) {  // 大根堆:x 的优先级更高,x 作为父节点
        tr[x].ri=merge(tr[x].ri,y);  // 把 y 合并到 x 的右子树
        pushup(x);                  // 更新 x 的 size
        return x;
    } else {  // y 的优先级更高,y 作为父节点
        tr[y].le=merge(x,tr[y].le);  // 把 x 合并到 y 的左子树
        pushup(y);                  // 更新 y 的 size
        return y;
    }
}

(1)核心逻辑:按堆性质(pri)决定父节点,按 BST 性质(val)决定子树位置;
(2)前提条件:x 的所有 val <= y 的所有 val(由 split 保证,所以 merge 是安全的);
(3)堆性质的作用:通过随机 pri 让合并后的树保持平衡,避免退化成链表。

​​​​​​​●​​​​​​​ insert:插入数值 u

void insert(int u) {
    int x=0,y=0;          // 初始化分裂后的左右子树为空
    split(root,u,x,y);    // 把原树按 u 分裂:x(<=u)、y(>u)
    // 合并:x + 新节点(u) + y → 新的根节点
    root=merge(merge(x,get_node(u)),y);
}

(1)步骤拆解:
分裂原树为 x(<=u)和 y(>u);
创建新节点存储 u;
先合并 x 和新节点,再合并结果和 y,得到新树;
(2)举例:插入 106465 时,原树为空,split 后 x=0、y=0,合并后 root 就是新节点的编号。

● remove:删除数值 u

void remove(int u) {
    int x=0,y=0,z=0;          // 初始化三个临时变量
    split(root,u,x,z);        // 第一步分裂:x(<=u)、z(>u)
    split(x,u-1,x,y);         // 第二步分裂:x(<u)、y(=u)
    y=merge(tr[y].le,tr[y].ri); // 合并 y 的左右子树 → 删掉 y 节点(仅删一个)
    root=merge(merge(x,y),z);  // 合并 x + 删后的 y + z → 新树
}

(1)核心思路:精准定位 “val=u” 的节点(y),通过合并其左右子树跳过 y,实现删除;
(2)步骤拆解:
第一次 split:把原树分成 <=u(x)和>u(z);
第二次 split:把 x 分成 <u(x)和 =u(y);
合并 y 的左右子树(相当于删除 y 节点);
重新合并 x、删后的 y、z,得到删除后的树;
(3)注意:此代码仅删除一个 u(若有重复值,需调整 split 逻辑)。

● get_rank_by_val:查询 x 的排名

void get_rank_by_val(int val) {
    int x=0,y=0;
    split(root,val-1,x,y);  // 分裂出 <val 的节点(x)、>=val 的节点(y)
    printf("%d\n",tr[x].size+1);  // 排名 = 比 val 小的数的个数 +1
    root=merge(x,y);        // 分裂后必须合并,恢复原树(否则树结构被破坏)
}

(1)原理:排名的定义是 “比 x 小的数的个数 +1”,而 tr[x].size 就是 “比 val 小的数的个数”;
(2)关键:split 用 val-1 作为阈值,确保 x 中只有 <val 的节点;
(3)举例:查询 106465 的排名时,原树只有 106465,split (val-1=106464) 后 x 为空(size=0),排名 = 0+1=1。

● get_val_by_rank:查询第 k 小的数

void get_val_by_rank(int rank) {
    int u=root;  // 从根节点开始遍历
    while(u) {   // 非递归遍历(避免递归栈溢出)
        if(tr[tr[u].le].size>=rank) {
            // 左子树大小 >= rank → 第 k 小在左子树
            u=tr[u].le;
        } else if(tr[tr[u].le].size+1==rank) {
            // 左子树大小 +1 = rank → 当前节点就是第 k 小
            printf("%d\n",tr[u].val);
            return;
        } else {
            // 第 k 小在右子树,调整 rank(减去左子树+自身)
            rank-=tr[tr[u].le].size+1;
            u=tr[u].ri;
        }
    }
}

(1)核心逻辑:利用 BST 性质和 size 定位第 k 小:
左子树 size = 比当前节点小的节点数;
若 rank ≤ 左子树 size → 目标在左子树;
若 rank = 左子树 size +1 → 目标是当前节点;
否则 → 目标在右子树,rank 减去左子树 + 自身的数量。
(2)举例:查询第 1 小的数时,根节点左子树 size=0 < 1,且 0+1=1,直接输出根节点的 val(106465)。

● get_prev:查询 x 的前驱(比 x 小的最大数)

void get_prev(int v) {
    int x=0,y=0;
    split(root,v-1,x,y);  // 分裂出 <v 的节点(x)、>=v 的节点(y)
    int k=x;
    while(tr[k].ri) k=tr[k].ri;  // 找 x 的最右节点(<v 的最大数)
    printf("%d\n",tr[k].val);
    root=merge(x,y);  // 恢复原树
}

(1)原理:前驱是 “<v 的最大数”,而 BST 中 “最右节点” 就是最大值;
(2)步骤:
split 出所有 <v 的节点(x);
遍历 x 找到最右节点(val 最大);
输出该节点的 val,合并恢复原树;
(3)举例:查询 493598 的前驱时,split (v-1=493597) 后 x 包含[84185,89851,106465,317721,460929,492737],最右节点是 492737。

● get_next:查询 x 的后继(比 x 大的最小数)

void get_next(int v) {
    int x=0,y=0;
    split(root,v,x,y);  // 分裂出 <=v 的节点(x)、>v 的节点(y)
    int k=y;
    while(tr[k].le) k=tr[k].le;  // 找 y 的最左节点(>v 的最小数)
    printf("%d\n",tr[k].val);
    root=merge(x,y);  // 恢复原树
}

(1)原理:后继是 “>v 的最小数”,BST 中 “最左节点” 就是最小值;
(2)步骤:
split 出所有 >v 的节点(y);
遍历 y 找到最左节点(val 最小);
输出该节点的 val,合并恢复原树;
(3)举例:查询 81968 的后继时,split (v=81968) 后 y 包含 [84185,89851,106465,...],最左节点是 84185。

【算法代码】

#include <bits/stdc++.h>
using namespace std;

const int maxn=1e5+5;
int root,idx;

struct node {
    int le,ri;
    int val,pri;
    int size;
} tr[maxn];

void pushup(int p) {
    tr[p].size=tr[tr[p].le].size+tr[tr[p].ri].size+1;
}

int get_node(int val) {
    tr[++idx].val=val;
    tr[idx].pri=rand();
    tr[idx].size=1;
    return idx;
}

void split(int u,int v,int &x,int &y) {
    if(!u) x=y=0;
    else {
        if(tr[u].val<=v) x=u,split(tr[u].ri,v,tr[u].ri,y);
        else y=u,split(tr[u].le,v,x,tr[u].le);
        pushup(u);
    }
}

int merge(int x,int y) {
    if(!x || !y) return x+y;
    if(tr[x].pri>tr[y].pri) {
        tr[x].ri=merge(tr[x].ri,y);
        pushup(x);
        return x;
    } else {
        tr[y].le=merge(x,tr[y].le);
        pushup(y);
        return y;
    }
}

void insert(int u) {
    int x=0,y=0;
    split(root,u,x,y);
    root=merge(merge(x,get_node(u)),y);
}

void remove(int u) {
    int x=0,y=0,z=0;
    split(root,u,x,z);
    split(x,u-1,x,y);
    y=merge(tr[y].le,tr[y].ri);
    root=merge(merge(x,y),z);
}

void get_rank_by_val(int val) {
    int x=0,y=0;
    split(root,val-1,x,y);
    printf("%d\n",tr[x].size+1);
    root=merge(x,y);
}

void get_val_by_rank(int rank) {
    int u=root;
    while(u) {
        if(tr[tr[u].le].size>=rank) u=tr[u].le;
        else if(tr[tr[u].le].size+1==rank) {
            printf("%d\n",tr[u].val);
            return;
        } else rank-=tr[tr[u].le].size+1,u=tr[u].ri;
    }
}

void get_prev(int v) {
    int x=0,y=0;
    split(root,v-1,x,y);
    int k=x;
    while(tr[k].ri) k=tr[k].ri;
    printf("%d\n",tr[k].val);
    root=merge(x,y);
}

void get_next(int v) {
    int x=0,y=0;
    split(root,v,x,y);
    int k=y;
    while(tr[k].le) k=tr[k].le;
    printf("%d\n",tr[k].val);
    root=merge(x,y);
}

int main() {
    int n,op,x;
    scanf("%d",&n);
    while(n--) {
        scanf("%d%d",&op,&x);
        if(op==1) insert(x);
        else if(op==2) remove(x);
        else if(op==3) get_rank_by_val(x);
        else if(op==4) get_val_by_rank(x);
        else if(op==5) get_prev(x);
        else get_next(x);
    }

    return 0;
}

/*
in:
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

out:
106465
84185
492737
*/



【参考文献】
https://blog.csdn.net/hnjzsyjyj/article/details/138482439
https://www.acwing.com/file_system/file/content/whole/index/content/3173659/
https://blog.csdn.net/hnjzsyjyj/article/details/120380473



 

posted @ 2026-01-27 14:23  Triwa  阅读(4)  评论(0)    收藏  举报