[SP3946]MKTHNUM - K-th Number

Posted -wallace-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[SP3946]MKTHNUM - K-th Number相关的知识,希望对你有一定的参考价值。

闲话

题解区一路翻下来居然没有归并树的题解?!那我来补一发吧。

对于像我这样的的juruo归并树当然是最好理解的。

分块在后面。

题解

在这道题之前,我们先来考虑这一个问题:

实现一种数据结构,支持静态查询区间([l,r])中有几个数(le x)

假设我们已经可以高效地处理这个问题了,那么我们就可以使用二分查找法在(O( ext{查询一次的时间} imeslog_2 n))的复杂度内得到答案。

对于上述问题,看到区间,我们考虑线段树。不过,这个并不是像以往的一样,一个结点存放一个值或几个值,而是 一个数组——代表这个结点的维护区域排好序的结果。

For example:{1,4,3,6,9,0,3,2}的维护结果:

技术图片

注:结点编号为:

技术图片

可以看出,这棵树相当于还原了归并排序的完整过程,因而得名归并树

这样,对于一颗建好的归并树,我们如何在区间([l,r])内找出(le x)的数的个数呢?

大概分几个步骤:

  • 若当前结点维护区域 ([l,r]) 所包含,那么显然我们可以用一次二分查找找到该维护区域内的第一个大于(x)的数(p),返回(p)前面的数的个数即可。

  • 若没有完全包含,那么分别查看左右子结点的维护区域 是否有交集 ,有的递归累计答案,没有的不管,最后返回累计后的答案。

For example:以上述归并树为例,(l=2,r=6,x=3)时:

  1. 走到根节点,发现并没有被([l,r])完全包含,且([l,r])与之左右子结点的维护区域均有交集,于是分别递归左右子结点,返回步骤2的答案+步骤3的答案=2;

  2. 走到结点2,发现并没有被([l,r])完全包含,且([l,r])与之左右子结点的维护区域均有交集,于是分别递归左右子结点,返回步骤4的答案+步骤5的答案=1;

  3. 走到结点3,发现并没有被([l,r])完全包含,且([l,r])仅与之左子结点的维护区域有交集,于是递归左子结点,返回步骤6的答案=1;

  4. 走到结点4,发现并没有被([l,r])完全包含,且([l,r])仅与之右子结点的维护区域有交集,于是递归右子结点,返回步骤7的答案=0;

  5. 走到结点5,发现被([l,r])完全包含,直接使用二分法,发现1个数(le x),返回1;

  6. 走到结点6,发现被([l,r])完全包含,二分发现有1个数(le x),返回1;

  7. 走到结点9,发现被([l,r])完全包含,二分发现有没有数(le x),返回0;

  • 最后答案为2

递归结果可能难以理解,请仔细斟酌。

代码分步实现

重要部分有二:建树&查询

变量定义 variables

int L[N<<2],R[N<<2];//结点维护的区间范围
vector<int> dat[N<<2];//结点维护区域上数列排序后的结果

建树 build

    void build(int l,int r,int rt=1)
    {
        L[rt]=l,R[rt]=r;//记录维护区间
        if(l==r)//结束递归的条件
        {
            dat[rt].resize(1);
            dat[rt][0]=Array[l];//叶子结点只存一个数
            return;
        }
        int mid=(l+r)>>1;
        build(l,mid,rt<<1),build(mid+1,r,rt<<1|1);//递归建树
        dat[rt].resize(r-l+1);
        merge(dat[rt<<1].begin(),dat[rt<<1].end(),
            dat[rt<<1|1].begin(),dat[rt<<1|1].end(),
            dat[rt].begin());//仿照归并排序有序合并两个子结点的排序后的结果
    }

查询 query

    int query(int l,int r,int x,int rt=1)//查询[l,r]中<=x的数的个数。
    {
        if(l<=L[rt]&&R[rt]<=r)//被完全包含
            return upper_bound(dat[rt].begin(),dat[rt].end(),x)
                  -dat[rt].begin();//二分法查找第一个>x的数的下标,由于是下标从0开始的vector,将这个下标直接返回即可。
        int mid=(L[rt]+R[rt])>>1,ret=0;
        if(l<=mid) ret+=query(l,r,x,rt<<1);//如果答案与左边有关,递归左边
        if(r>mid) ret+=query(l,r,x,rt<<1|1);//如果答案与右边有关,递归右边
        return ret;//返回答案
    }

时间复杂度

以下说法是对于原题(查询k-th)的。

建树的复杂度与归并排序相同,为(O(nlog_2 n))

查询(le x)的数的个数的复杂度为普通线段树的复杂度( imes)二分的复杂度,约为(O(log_2^2 n))

真正的查询还要加一个二分,因此 总复杂度(O(nlog n + mlog^3 n)),应该是比较优秀了,可以通过此题。

完整代码

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;

const int N=1e5+7;
int Array[N];
namespace SGTree{
    int L[N<<2],R[N<<2];
    vector<int> dat[N<<2];
    
    void build(int l,int r,int rt=1)
    {
        L[rt]=l,R[rt]=r;
        if(l==r)
        {
            dat[rt].resize(1);
            dat[rt][0]=Array[l];
            return;
        }
        int mid=(l+r)>>1;
        build(l,mid,rt<<1),build(mid+1,r,rt<<1|1);
        dat[rt].resize(r-l+1);
        merge(dat[rt<<1].begin(),dat[rt<<1].end(),
              dat[rt<<1|1].begin(),dat[rt<<1|1].end(),
              dat[rt].begin());
    }
    int query(int l,int r,int x,int rt=1)//count numbers which <=x
    {
        if(l<=L[rt]&&R[rt]<=r)
            return upper_bound(dat[rt].begin(),dat[rt].end(),x)
                  -dat[rt].begin();
        int mid=(L[rt]+R[rt])>>1,ret=0;
        if(l<=mid) ret+=query(l,r,x,rt<<1);
        if(r>mid) ret+=query(l,r,x,rt<<1|1);
        return ret;
    }
}
int n,m;

int Solve(int i,int j,int k)
{
    int lb=0,ub=n;
    while(ub-lb>1)
    {
        int mid=(lb+ub)>>1;
        int cnt=SGTree::query(i,j,Array[mid]);
        if(cnt>=k) ub=mid;
        else lb=mid;
    }
    return Array[ub];
}

signed main()
{
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        for(register int i=1;i<=n;i++)
            scanf("%d",Array+i);
        SGTree::build(1,n);
        sort(Array+1,Array+1+n);
        int i,j,k;
        while(m--)
        {
            scanf("%d%d%d",&i,&j,&k);
            printf("%d
",Solve(i,j,k));
        }
    }
    return 0;
}

总结

线段树在处理区间问题时是一个利器,熟练了可以受益匪浅,常做线段树的题还可以锻炼思维。有些题线段树虽然复杂度不一定是最优的,但是它代码简单,易于实现,所以掌握它还是很必要的。

AC record:https://www.luogu.com.cn/record/29929630

拓展:分块

众所周知,线段树能做的大部分分块都可以做,那么这题也是显然。这里我将分块做法也 提一下(?)。

我们将数列分为(t)块,每个块放(dfrac{n}{t})个排好序的数。维护([l,r])的块存的是(A[l...r])的排序后的结果。这里块推荐使用vector。

那么对于每次查询,我们同样将它分成二分和找(le x)的数的个数两个部分。

二分不讲了,要查询([l,r])(le x)的数的个数:

  • 整块:二分法,原理同线段树;

  • 散块:在 原数组上 暴力查找。

这样一下(包括二分)是(O(tlog (dfrac{n}{t})+dfrac{n}{t}))

预处理复杂度(O(nlog (dfrac{n}{t})))

总复杂度(O(nlog (dfrac{n}{t}) +mtlog(dfrac{n}{t})+dfrac{nm}{t}))

注:由于查询在整块与散块的复杂度不同,所以(t=sqrt{n})不一定是最佳选择,一种参考值是(t=dfrac{sqrt{n}}{log ^{0.5}n})

参考

  • 《挑战程序设计竞赛(第2版)》3.3

完结撒花。。。。有错误请在评论请指正。

求赞(QwQ)

以上是关于[SP3946]MKTHNUM - K-th Number的主要内容,如果未能解决你的问题,请参考以下文章

Spoj MKTHNUM - K-th Number

luogu P3946 ことりのおやつ 题解

python第11天作业

luogu 3946 ことりのおやつ(小鸟的点心)

K-th Number

K-th Number