第K小数

第K小数

题目描述

在这里插入图片描述


核心思路

因为序列A的下标最大为 N = 1 0 5 N=10^5 N=105,然而最坏情况下 A [ i ] = 1 0 9 A[i]=10^9 A[i]=109,因此我们需要先对序列A进行离散化,让 A [ i ] A[i] A[i]的下标都落在0到 N − 1 N-1 N1之间。

先将数组里面每个数进行离散化,用可持久化线段树维护每一个数值出现的次数,比如某个节点所维护区间中所含有的序列为{24,3,24,99,13,99,23,99},那么数值3出现1次,数值13出现1次,数值23出现1次,数值24出现2次,数值99出现3次,那么这个节点所维护区间中出现的数值的总个数为1+1+1+2+3=8。从左到右每增加一个数值,线段树增加一个版本,由此我们想到了可持久化线段树。对于一个区间[0, M]而言,可以求出 任何一个版本下,离散化后数值在[0, M]中的数字的总个数。题目要求第L个数到第R个数中第K小的数值是多少,可以在第L-1个版本和第R个版本分别求区间[0,M]中的数值总数,两者的差就是原来的第L个数到第R个数, 离散化后的数值落在数值区间[0, M]的总个数,如果这个个数刚好就是K,那就等价于找到了一个合法的M,用二分法查找最小的一个合法的M,再把M的数值映射回原始的数值即可。

先考虑如何求总序列第k小

我们可以建立一颗权值线段树,每个点存储的信息为该值域区间存在的数的个数

在这里插入图片描述

例如该图,节点2代表的是值域为 [ 1 , 2 ] [1,2] [1,2]的区间,节点6代表值域为 [ 3 , 4 ] [3,4] [3,4]的区间。

可持久化的概念:可持久化实质上就是存储该数据结构所有的历史状态,以达到高效的处理某些信息的目的

可持久化线段树主要用来解决什么样的问题呢,就像下面这个:

  • 单点修改
  • 查询在第x次修改前的区间和

因为要查询在第x操作前的区间和,那么我们肯定要能够用一种奇妙的做法来保存第x次操作前的线段树,那怎么保存呢?首先,肯定是不能对每一次操作新建一棵线段树,因为线段树本来就是很耗空间的一个东西,那么我们细细想一下,发现每一次只会修改线段树上的一条链,也就是说,我们不需要新建整整一棵线段树,只需要新建这一条链上的节点就好了!就比如假如我们有一个数列{2,4,1,3},那么建出来的线段树就是这样的(节点上的值代表自己管理范围内的和,红色字为编号):

在这里插入图片描述

因为线段树的性质,所以每个点的左子树的值域区间$\leq 右 子 树 的 值 域 区 间 。 因 此 , 我 们 可 以 先 计 算 出 左 子 树 区 间 内 共 有 多 少 个 数 , 不 妨 记 为 右子树的值域区间。因此,我们可以先计算出左子树区间内共有多少个数,不妨记为 cnt$

  • 如果 k ≤ c n t k\leq cnt kcnt,就说明第 k k k小的数一定是在左子树的值域内,即在左子树区间中。于是问题就转化为"在左子树的值域内(左子树区间)找到第 k k k小的数"。
  • 如果 k > c n t k>cnt k>cnt,就说明第 k k k小的数一定是在右子树的值域内,即在右子树区间中。因为我们已经求出了左子树区间中有 c n t cnt cnt个数字,那么这个第 k k k小的数在右子树区间就是第 k − c n t k-cnt kcnt小的数,于是问题就转化为"在右子树的值域内(右子树区间)找第 k − c n t k-cnt kcnt小的数"。

问题转换到任意区间

我们要用 [ l i , r i ] [l_i,r_i] [li,ri]区间的数建立权值线段树,我们发现可以用前缀和来维护:只要用预处理大法分别以 [ 1 , l i ] [1,l_i] [1,li] [ 1 , r i ] [1,r_i] [1,ri]的数建立权值线段树,每个点的值对位相减即可。

关键性质

我们发现 [ 1 , x ] [1,x] [1,x] [ 1 , x + 1 ] [1,x+1] [1,x+1]区间内的数所建立的权值线段树的差异仅在一条链上: A [ x + 1 ] A[x+1] A[x+1]的次数+1。也就是不超过 l o g 2 n log_2n log2n个点。可以考虑动态开点:

  • 与上一个权值线段树没有差异的地方直接指引过去
  • 有差异,单独新增一个点

这样就可以预处理出 [ 1 , x ] ( 1 ≤ x ≤ n ) [1,x](1\leq x\leq n) [1,x](1xn)所有的权值线段树了。

暂时不考虑题目询问的区间 [ l i , r i ] [l_i,r_i] [li,ri]。如果能够在线段树上维护"序列A有多少个数落在值域区间[L,R]内(记为 c n t L , R cnt_{L,R} cntL,R)",那么只需要比较 c n t L , m i d cnt_{L,mid} cntL,mid与k的大小关系,即可确定第k小数是 ≤ m i d \leq mid mid还是 > m i d > mid >mid,从而进入线段树的左、右子树。在有 l i , r i l_i,r_i liri限制下,可以用可持久化线段树。

我们首先对序列A进行离散化,设离散化后A[i]的值为 H [ A [ i ] ] ∈ [ 1 , T ] H[A[i]]\in [1,T] H[A[i]][1,T]。在区间 [ 1 , T ] [1,T] [1,T]上建立可持久化线段树,线段树上的每一个节点都保存一个值cnt,表示该节点代表的值域区间[L,R]中一共插入了多少个数,初始化cnt=0。然后我们对每一个A[i],在可持久化线段树上执行 H [ A [ i ] ] H[A[i]] H[A[i]]的"单点修改",将其cnt加1.线段树每个内部节点的cnt值=左子节点的cnt+右子节点的cnt。此时,可持久化线段树中"以root[i]为根的线段树"的值域区间[L,R],就保存了A的前i个数有多少个落到了值域区间[L,R]中。

接下来考虑每个询问 l i , r i l_i,r_i li,ri。这里有一个重要的性质: r o o t [ l i ] root[l_i] root[li]和以 r o o t [ r i ] root[r_i] root[ri]为根的两棵线段树对值域的划分是相同。换言之,除了cnt值不同之外,两颗线段树的内部结构和每个节点代表的值域区间完全对应。这意味着" r o o t [ r i ] root[r_i] root[ri]的值域区间[L,R]的cnt值"减去" r o o t [ l i ] root[l_i] root[li]的值域区间[L,R]的cnt值"就等于 A [ l i ]   A [ r i ] A[l_i]~A[r_i] A[li] A[ri]中有多少个数落在值域区间[L,R]内,也就是说可持久化线段树中两个代表相同值域的节点具有可减性。


代码

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=100010;
int n,m;
int a[N];
int root[N],idx;
vector<int>nums;
struct Node{
    //因为可持久化线段树它已经不再是一棵完全二叉树了,所以我们不能用层次序编号,而是改为直接记录每个节点
    //的左右子节点的编号,就与trie的字符指针类似。
    int l,r;    //l是左子节点的编号,r是右子节点的编号
    int cnt;//表示该节点所代表的值域区间[L,R]中一共插入了多少个数
}tr[4*N+17*N];
//二分找到x离散化后的值
int find(int x)
{
    return lower_bound(nums.begin(),nums.end(),x)-nums.begin();
}
int build(int start,int end)
{
    int p=++idx;    //给p分配一个节点编号
    //此时到了叶子节点,返回p的节点编号
    if(start==end)
    return p;
    int mid=(start+end)/2;
    tr[p].l=build(start,mid);//递归创建左子树
    tr[p].r=build(mid+1,end);//递归创建右子树
    return p;
}
//在区间[l,r]中插入x
int modify(int p,int l,int r,int x)
{
    int q=++idx;//给q分配一个节点编号
    tr[q]=tr[p];//q是新版本,p的旧版本,q要继承p拥有的东西
    //如果到了叶子节点
    if(l==r)
    {
        tr[q].cnt++;//此时区间长度为1,里面只有一个数,因此cnt+1
        return q;
    }
    int mid=(l+r)/2;
    //如果x小于等于mid,递归左子树
    if(x<=mid)
    tr[q].l=modify(tr[q].l,l,mid,x);
    else
    //如果x大于mid,递归右子树
    tr[q].r=modify(tr[q].r,mid+1,r,x);
    //tr[q].l是内部节点q的左子节点编号,tr[q].r是内部节点q的右子节点编号
    //内部节点q的cnt=左子节点的cnt+右子节点的cnt
    tr[q].cnt=tr[tr[q].l].cnt+tr[tr[q].r].cnt;
    return q;
}
//q是第r个版本的,p是第l-1个版本的可持久化线段树,在区间[l,r]中查询第k小的数
int query(int q,int p,int l,int r,int k)
{
    //如果到了叶子节点,此时只有一个数,返回即可
    if(l==r)
    return r;
    int mid=(l+r)/2;
    //先算出在区间[l,r]内,第r版本的左区间的cnt减去第l-1版本的左区间的cnt
    //即可知道序列a由多少个数落在值域区间[l,r]的左边
    int cnt=tr[tr[q].l].cnt-tr[tr[p].l].cnt;
    //如果k小于等于cnt,说明第k小的数是在左区间
    if(k<=cnt)
    return query(tr[q].l,tr[p].l,l,mid,k);
    //k>cnt,说明第k小的数是在右区间的第k-cnt个
    else
    return query(tr[q].r,tr[p].r,mid+1,r,k-cnt);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        nums.push_back(a[i]);
    }
    sort(nums.begin(),nums.end());  //排序
    nums.erase(unique(nums.begin(),nums.end()),nums.end()); //去重
    root[0]=build(0,nums.size()-1); //先建立第0个版本的可持久化线段树
    //依次输入这n个数,然后建立n个版本的可持久化线段树
    for(int i=1;i<=n;i++)
    root[i]=modify(root[i-1],0,nums.size()-1,find(a[i]));
    while(m--)
    {
        int l,r,k;
        scanf("%d%d%d",&l,&r,&k);
        int i=query(root[r],root[l-1],0,nums.size()-1,k);
        printf("%d\n",nums[i]);
    }
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值