题解:洛谷 P3369 【模板】普通平衡树

【题目来源】

洛谷:P3369 【模板】普通平衡树 - 洛谷

【题目描述】

您需要动态地维护一个可重集合 \(M\),并且提供以下操作:

  1. \(M\) 中插入一个数 \(x\)
  2. \(M\) 中删除一个数 \(x\)(若有多个相同的数,应只删除一个)。
  3. 查询 \(M\) 中有多少个数比 \(x\) 小,并且将得到的答案加一。
  4. 查询如果将 \(M\) 从小到大排列后,排名位于第 \(x\) 位的数。
  5. 查询 \(M\)\(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。
  6. 查询 \(M\)\(x\) 的后继(后继定义为大于 \(x\),且最小的数)。

对于操作 \(3,5,6\)不保证当前可重集中存在数 \(x\)

对于操作 \(5,6\),保证答案一定存在。

【输入】

第一行为 \(n\),表示操作的个数,下面 \(n\) 行每行有两个数 \(\text{opt}\)\(x\)\(\text{opt}\) 表示操作的序号($ 1 \leq \text{opt} \leq 6 $)。

【输出】

对于操作 \(3,4,5,6\) 每行输出一个数,表示对应答案。

【输入样例】

10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

【输出样例】

106465
84185
492737

【算法标签】

《洛谷 P3369 普通平衡树》 #平衡树# #模板题#

【代码详解】

// 使用FHQ Treap再写一遍
#include <bits/stdc++.h>
using namespace std;

const int N = 100005;  // 数组最大容量
int n;  // 操作次数

// Treap节点结构体
struct Node
{
    int l, r;  // 左右儿子节点编号
    int val;   // 节点权值
    int key;   // 堆的随机值
    int size;  // 子树大小
}tr[N];

int root;  // 根节点编号
int idx;   // 节点个数计数器

// 创建新节点
int newnode(int v)
{
    tr[++idx].val = v;
    tr[idx].key = rand();
    tr[idx].size = 1;
    return idx;
}

// 更新节点信息
void pushup(int p)
{
    tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + 1;
}

// 分裂:将Treap按值v分裂为两个Treap
void split(int p, int v, int &x, int &y)
{
    if (!p)
    {
        x = y = 0;
        return ;
    }
    if (tr[p].val <= v)
    {
        x = p;
        split(tr[x].r, v, tr[x].r, y);
    }
    else
    {
        y = p;
        split(tr[y].l, v, x, tr[y].l);
    }
    pushup(p);
}

// 合并:将两个Treap合并
int merge(int x, int y)
{
    if (!x || !y)  // 如果有一个为空,返回另一个
    {
        return x + y;
    }
    if (tr[x].key < tr[y].key)  // 维护堆性质
    {
        tr[x].r = merge(tr[x].r, y);
        pushup(x);
        return x;
    }
    else
    {
        tr[y].l = merge(x, tr[y].l);
        pushup(y);
        return y;
    }
}

// 插入值v
void insert(int v)
{
    int x, y, z;
    split(root, v, x, y);  // 按v分裂
    z = newnode(v);  // 创建新节点
    root = merge(merge(x, z), y);  // 合并
}

// 删除值v
void del(int v)
{
    int x, y, z;
    split(root, v, x, z);  // 按v分裂
    split(x, v - 1, x, y);  // 按v-1分裂,得到包含v的子树y
    y = merge(tr[y].l, tr[y].r);  // 删除y的根节点
    root = merge(merge(x, y), z);  // 合并
}

// 返回第k个节点
int get_k(int p, int k)
{
    if (k <= tr[tr[p].l].size)
    {
        return get_k(tr[p].l, k);
    }
    if (k == tr[tr[p].l].size + 1)
    {
        return p;
    }
    return get_k(tr[p].r, k - tr[tr[p].l].size - 1);
}

// 查找v的前驱
void get_pre(int v)
{
    int x, y;
    split(root, v - 1, x, y);  // 按v-1分裂
    int p = get_k(x, tr[x].size);  // 找到x中最大的节点
    cout << tr[p].val << endl;
    root = merge(x, y);  // 合并
}

// 查找v的后继
void get_suc(int v)
{
    int x, y;
    split(root, v, x, y);  // 按v分裂
    int p = get_k(y, 1);  // 找到y中最小的节点
    cout << tr[p].val << endl;
    root = merge(x, y);  // 合并
}

// 查询v的排名
void get_rank(int v)
{
    int x, y;
    split(root, v - 1, x, y);  // 按v-1分裂
    cout << tr[x].size + 1 << endl;  // 小于v的节点数+1
    root = merge(x, y);  // 合并
}

// 查询第k小的值
void get_val(int k)
{
    int p = get_k(root, k);
    cout << tr[p].val << endl;
}

int main()
{
    cin >> n;  // 读入操作次数
    
    srand(time(0));  // 初始化随机数种子
    
    while (n--)
    {
        int op, x;
        cin >> op >> x;
        if (op == 1)  // 插入
        {
            insert(x);
        }
        if (op == 2)  // 删除
        {
            del(x);
        }
        if (op == 3)  // 查询排名
        {
            get_rank(x);
        }
        if (op == 4)  // 查询第k小
        {
            get_val(x);
        }
        if (op == 5)  // 查询前驱
        {
            get_pre(x);
        }
        if (op == 6)  // 查询后继
        {
            get_suc(x);
        }
    }
    return 0;
}

【运行结果】

10
1 106465
4 1
106465
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
84185
1 492737
5 493598
492737
posted @ 2026-02-19 14:34  团爸讲算法  阅读(3)  评论(0)    收藏  举报