码迷,mamicode.com
首页 > 其他好文 > 详细

[SP3946]MKTHNUM - K-th Number

时间:2020-02-06 12:16:41      阅读:56      评论:0      收藏:0      [点我收藏+]

标签:img   线段树   列排序   amp   分块   number   return   左右   http   

闲话

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

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

分块在后面。

题解

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

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

假设我们已经可以高效地处理这个问题了,那么我们就可以使用二分查找法在\(O(\text{查询一次的时间}\times\log_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(n\log_2 n)\)

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

真正的查询还要加一个二分,因此 总复杂度\(O(n\log n + m\log^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\n",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(t\log (\dfrac{n}{t})+\dfrac{n}{t})\)

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

总复杂度\(O(n\log (\dfrac{n}{t}) +mt\log(\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

标签:img   线段树   列排序   amp   分块   number   return   左右   http   

原文地址:https://www.cnblogs.com/-Wallace-/p/12267934.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!