洛谷 P5076【深基 16.例 7】普通二叉树(简化版)题解

本文最后更新于 2025年3月2日 下午

引用P5076【深基 16.例 7】普通二叉树(简化版) 题目描述 您需要写一种数据结构,来维护一些数(都是绝对值 $10^9$ 以内的数)的集合,...

P5076【深基 16.例 7】普通二叉树(简化版)

题目描述

您需要写一种数据结构,来维护一些数(都是绝对值 $10^9$ 以内的数)的集合,最开始时集合是空的。其中需要提供以下操作,操作次数 $q$ 不超过 $10^4$:

  1. 定义数 $x$ 的排名为集合中小于 $x$ 的数的个数 $+1$。查询数 $x$ 的排名。注意 $x$ 不一定在集合里
  2. 查询排名为 $x(x\ge 1)$ 的数。保证集合里至少有 $x$ 个数
  3. 求 $x$ 的前驱(前驱定义为小于 $x$,且最大的数)。若不存在则输出 $-2147483647$。
  4. 求 $x$ 的后继(后继定义为大于 $x$,且最小的数)。若不存在则输出 $2147483647$。
  5. 插入一个数 $x$,本题的数据保证插入前 $x$ 不在集合中。

保证执行 $1,3,4$ 操作时,集合中有至少一个元素。

输入格式

第一行是一个整数 $q$,表示操作次数。

接下来 $q$ 行,每行两个整数 $op,x$,分别表示操作序号以及操作的参数 $x$。

输出格式

输出有若干行。对于操作 $1,2,3,4$,输出一个整数,表示该操作的结果。

输入输出样例 #1

输入 #1

7
5 1
5 3
5 5
1 3
2 2
3 3
4 3

输出 #1

2
3
1
5

题解

方法 1

使用 BST

一个实现如下

代码CPP · 134 行
#include<bits/stdc++.h>
using namespace std;

using i64=long long;
using u64=unsigned long long;
using u32=unsigned;
using u128=unsigned __int128;

const int inf=2147483647;

//需要实现:按照排名找权值,按照权值找排名,找前驱,找后继,插入
struct node{
    int v;
    int lchild=0,rchild=0;
    int siz; //(两子树 + 自己本身) 的大小(结点数之和)
}bst[100010]; //以 1 为根结点

int cnt=0; //表示总结点数目

void insert(int v,int idx){ //v 表示权值,idx 表示当前的结点编号
    bst[idx].siz++;
    if(v>bst[idx].v){ //说明 v 应该插入到右子树
        if(bst[idx].rchild!=0){
            insert(v,bst[idx].rchild);
        }
        else{ //不存在右孩子
            cnt++;
            bst[cnt].v=v;
            bst[cnt].siz=1;
            bst[idx].rchild=cnt;
        }
    }
    else{
        if(bst[idx].lchild!=0){
            insert(v,bst[idx].lchild);
        }
        else{
            cnt++;
            bst[cnt].v=v;
            bst[cnt].siz=1;
            bst[idx].lchild=cnt;
        }
    }
}

int findPre(int v){
    int idx=1; //从根结点开始搜索
    int ans=-inf;
    while(idx){
        if(bst[idx].v<v){
            ans=bst[idx].v;
            idx=bst[idx].rchild;
        }
        else{
            idx=bst[idx].lchild;
        }
    }
    return ans;
}

int findPost(int v){
    int idx=1;
    int ans=inf;
    while(idx){
        if(bst[idx].v>v){
            ans=bst[idx].v;
            idx=bst[idx].lchild;
        }
        else{
            idx=bst[idx].rchild;
        }
    }
    return ans;
}

int fromRankfindV(int rk,int idx){ //idx 是当前遍历到的根结点
    int leftSize=bst[bst[idx].lchild].siz;
    if(rk<=leftSize){
        return fromRankfindV(rk,bst[idx].lchild);
    }
    else if(rk==leftSize+1){
        return bst[idx].v;
    }
    else{
        return fromRankfindV(rk-leftSize-1,bst[idx].rchild);
    }
    
}

//标准做法
int fromVfindRank(int v,int idx){ //最后返回值还需要再 +1
    if(idx==0) return 0;
    if(v<=bst[idx].v){
        return fromVfindRank(v,bst[idx].lchild);
    }
    else{
        return bst[bst[idx].lchild].siz+1+fromVfindRank(v,bst[idx].rchild);
    }
}

int main(){
    ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);

    int q;
    cin>>q;

    while(q--){
        int op;
        int x;
        cin>>op>>x;
        if(op==1){
            cout<<fromVfindRank(x,1)+1<<"\n";
        }
        else if(op==2){
            cout<<fromRankfindV(x,1)<<"\n";
        }
        else if(op==3){
            cout<<findPre(x)<<"\n";
        }
        else if(op==4){
            cout<<findPost(x)<<"\n";
        }
        else if(op==5){
            if(cnt==0){
                cnt=1;
                bst[1].v=x;
                bst[1].siz=1;
            }
            else insert(x,1);
        }
    }

    return 0;
}

但是找前驱和找后继的函数可以省略,因为可以先查询 x 的排名 rank,然后查询排名为 rank-1 和 rank+1 的数,就分别是前驱和后继了。

如果 rank+1>cnt 或者 rank-1<1,那么就可以分别输入 inf 和-inf

方法 2

使用 STL 容器 multiset

引用multiset 的常见用法详解 multiset 的定义 multiset 是一个集合容器,它可以存储多个相同的元素。换句话说,如果你有一堆重...

multiset 的常见用法详解

multiset 的定义

multiset 是一个集合容器,它可以存储多个相同的元素。换句话说,如果你有一堆重复的数,比如 3、5、5、5、7,你可以用multiset来妥善管理它们。与set不同的是,set不会允许重复的元素,而multiset非常欢迎重复元素的加入。

可以使用统一初始化的元素来创建

int main() {
    multiset<int> numbers = {3, 5, 5, 7, 3, 9};

    // 基于范围的 for 循环遍历 multiset
    for (int num : numbers) {
        cout << num << " ";
    }

    return 0;
}

注意,multiset会自动按升序排列元素。

如果是自定义类型,需要重载小于号

multiset 内元素的访问

使用迭代器

multiset 常用函数实例解析

insert()

略了

erase()

erase方法用来删除元素。但是要注意!如果你直接传入一个值,所有与这个值相同的元素都会被删除。如果你只想删除一个特定的元素,最好先找到它的迭代器,然后用这个迭代器来删除。

int main() {
    multiset<int> numbers = {3, 5, 5, 7};

    // 删除所有的 5
    numbers.erase(5);

    // 打印当前的 multiset
    for (int num : numbers) {
        cout << num << " ";
    }

    return 0;
}

如果只想删除第一个 5

auto it = numbers.find(5);  // 找到第一个 5
if (it != numbers.end()) {
    numbers.erase(it);  // 只删除找到的第一个 5
}

size()

略了

clear()

略了

empty()

lower_bound()

和 algorithm中的不一样

upper_bound()

和 algorithm中的不一样

分析题目

1. 查询 x 数的排名

排名,说白了就是排序之后的 x 的下标。

我们只要用 lower_bound 方法,找到第一个 x 的位置。

然后从 begin 开始往后遍历容器,只要达到这个位置,就输出当前下标即可。

2.查询排名为 x 的数

遍历容器,只要当前排名到达 x,就输出当前值。

(因为 multiset 容器无法进行随机访问)

3.求 x 的前驱(前驱定义为小于 x,且最大的数)

前驱,也就是 x 的前一个。

我们只要用 lower_bound 方法找到第一个 x 的位置,然后输出上一个就 OK 了。

4.求 x 的后继(后继定义为大于 x,且最小的数)。

后继,也就是第一个大于 x 的数。

我们可以用 upper_bound 方法,直接找到这个值。

5.插入一个数 x

直接用 insert 方法插入即可。

代码CPP · 75 行
#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
#include<set>
using namespace std;
multiset<int>q;
int n,t,x,order;
int main()
{
    q.insert(-0x7fffffff);
    q.insert(0x7fffffff);
    //提前放入这两个数,避免错误
    scanf("%d",&n);
    while(n--)
    {
        scanf("%d%d",&t,&x);
        if(t==1)
        {
            auto it=q.lower_bound(x);
            //可以写作 multiset<int>::iterator,因为 lower_bound 方法返回的是迭代器
            // it 取得 x 的位置
            
            order=0;
            //order 为排名
            
            for(auto i=q.begin();i!=it;i++,order++);
            //这里就处理出了 x 的排名——order
            
            printf("%d\n",order);
            //输出 order 即为答案
        }
        else if(t==2)
        {
            order=-1;
            //初值为 -1 是因为前面有一个 -0x7fffffff,所以 order 要多跑一步

            for(int i:q)
                if(++order==x)
                //缩写,order 先自增一,再判断是否与 x 相等
                //如果是 (order++==x),那就是先判断再自增,这里要尤其注意
                    printf("%d\n",i);
                //i 就是容器里的值,输出 i
        }
        else if(t==3)
        {
            auto it=q.lower_bound(x);
            //取得第一个大于等于 x 的值
            //也就是第一个 x 的位置
            //由于我们要取得前驱,所以 it 要自减一
            printf("%d\n",*--it);
            //这句是先自减,再输出,是缩写
            //等价于:
            /*
                it--;
                printf("%d\n",*it);
            */
            //因为是迭代器(指针),所以输出前面加 *
        }
        else if(t==4)
        {
            printf("%d\n",*q.upper_bound(x));
            //要取得后继,就是第一个大于 x 的值
            //用 upper_bound 方法取得第一个大于 x 的迭代器
            //输出即可
            //因为是迭代器(指针),所以输出前面加 *
        }
        else
        {
            q.insert(x);
            //直接添加即可
        }
    }
    return 0;
}