zoukankan      html  css  js  c++  java
  • SPOJ MKTHNUM & POJ 2104

    题目链接:http://poj.org/problem?id=2104

    Description

    You are working for Macrohard company in data structures department. After failing your previous task about key insertion you were asked to write a new data structure that would be able to return quickly k-th order statistics in the array segment. 
    That is, given an array a[1...n] of different integer numbers, your program must answer a series of questions Q(i, j, k) in the form: "What would be the k-th number in a[i...j] segment, if this segment was sorted?" 
    For example, consider the array a = (1, 5, 2, 6, 3, 7, 4). Let the question be Q(2, 5, 3). The segment a[2...5] is (5, 2, 6, 3). If we sort this segment, we get (2, 3, 5, 6), the third number is 5, and therefore the answer to the question is 5.

    Input

    The first line of the input file contains n --- the size of the array, and m --- the number of questions to answer (1 <= n <= 100 000, 1 <= m <= 5 000). 
    The second line contains n different integer numbers not exceeding 109 by their absolute values --- the array for which the answers should be given. 
    The following m lines contain question descriptions, each description consists of three numbers: i, j, and k (1 <= i <= j <= n, 1 <= k <= j - i + 1) and represents the question Q(i, j, k).

    Output

    For each question output the answer to it --- the k-th number in sorted a[i...j] segment.

    Sample Input

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

    Sample Output

    5
    6
    3

    Hint

    This problem has huge input,so please use c-style input(scanf,printf),or you may got time limit exceed.

    题意:

    写一个数据结构,能够快速返回指定区间内的第k小元素。

    即:数组a[1:n]包含n个不同的整数,Q(i,j,k)代表询问:“若a[i:j]中元素从小到大排列,则其中第k个元素是什么?”

    例如:数组a = (1, 5, 2, 6, 3, 7, 4),现有查询Q(2, 5, 3),由于a[2:5]为(5, 2, 6, 3),升序排列后为(2, 3, 5, 6),第3个元素为5,所以Q(2, 5, 3) = 5

    数据范围:

    数组内元素个数n:1 ≤ n ≤ 100 000;

    查询个数m:1 ≤ m ≤ 5 000;

    元素大小:[-1e9,1e9];

    对于询问Q(i,j,k):1 ≤ i ≤ j ≤ n, 1 ≤ k ≤ j - i + 1;

    题解:


    主席树参考:

      https://www.bilibili.com/video/av4619406/

      https://blog.csdn.net/regina8023/article/details/41910615

    通俗易懂地讲解一下:

    对于一个数字串 $s$,假设其长度为 $n$,我们考虑枚举其前缀子串:

    对于第 $i(0 le i le n)$ 个前缀子串(即前 $i$ 个数),我们开一个 $cnt[...]$ 数组,用来记录这个子串里,每个数出现了几次。

    例如:

    $s = (1,3,4,3,5,1,5)$,此时 $n = 7$,考虑其:

    第 $0$ 个前缀子串:$pre_0 = ()$ 为空,此时 $cnt[...]$ 数组的值全为 $0$;

    第 $1$ 个前缀子串:$pre_1 = (1)$,此时 $cnt[1] = 1$,其他任意的 $cnt[i] = 0$,即仅有数字 $1$ 出现了一次;

    第 $2$ 个前缀子串:$pre_1 = (1,3)$,此时 $cnt[1] = 1,cnt[3] = 1$,其他任意的 $cnt[i] = 0$,即数字 $1$ 出现了一次,数字 $3$ 出现了一次;

    以此类推直到第 $7$ 个前缀子串:$pre_7 = s = (1,3,4,2,5,8,6)$,此时 $cnt[1] = 2,cnt[3] = 2,cnt[4] = 1,cnt[5] = 2$。

    是不是很简单?

    然后,我们不妨将cnt数组看做一条线段(这条线段的长度应当为 $O(n)$,考虑最坏情况,$s$ 内的每个数字均不重复出现,那么离散化后值域就应当是 $1 sim n$),然后我们用线段树来维护这条线段,

    然后你就想着,诶不对啊,这不行啊,原来的cnt数组,如果想要存储所有前缀子串的值不丢失的话,就已经是 $n imes n$ 的大小了,现在你还不够还要开 $n$ 个维护长度为 $n$ 的线段的线段树?

    岂不是内存原地爆炸?

    确实是的,这样存当然太大了,但是,我们很容易就能发现减小内存消耗的方式:

    从第 $i$ 个前缀子串到第 $i+1$ 个前缀子串,我实际上只有一个点的cnt值加了 $1$,其他的点的cnt值都是不变的,而这一个点所产生的变化会一直向上传递影响直到树根节点,所以我们可以仅仅新建并存储一条路径,而非一整棵树。

    解决了存储问题,我们就可以心安理得地假装:我现在已经有了 $n$ 棵维护长度为 $n$ 的线段的线段树了!

    可是这样做有啥子好处呢?

    也很简单,我们不妨回到最初的起点:我们就是想查询:数字串 $s$ 中任意区间内每个数字出现的次数。

    可以想见,如果使用纯暴力做法,对于任意的查询 $[l,r]$,最坏肯定是 $O(n)$ 的时间复杂度才能得到答案,这太慢了,无法接受。

    然后我们再来瞧瞧我们的前缀子串配合cnt数组能不能发挥点作用?

    我们不妨假设第 $i$ 个前缀子串的cnt数组是:$cnt[i][...]$,也就是说,第 $i$ 个前缀子串中数字 $num$ 出现的次数为 $cnt[i][num]$

    那我们就能 $O(1)$ 查询区间 $[l,r]$ 内某个数字 $num$ 出现的次数:$cnt[r][num]-cnt[l-1][num]$,

    现在加大难度,如果我现在想知道区间 $[l,r]$ 内连续若干个数字 $num,num+1,num+2,...,num+p$ 的出现次数和呢?

    难道要 $(cnt[r][num]-cnt[l-1][num]) + (cnt[r][num+1]-cnt[l-1][num+1]) + cdots + (cnt[r][num+p]-cnt[l-1][num+p])$ 吗?这样最坏情况取 $p=n$ 的话又要 $O(n)$ 了,无法接受。

    这就是我们要更进一步,使用线段树维护cnt数组的原因,

    先将上式变形:

    $egin{array}{l} (cnt[r][num] - cnt[l - 1][num]) + (cnt[r][num + 1] - cnt[l - 1][num + 1]) + cdots + (cnt[r][num + p] - cnt[l - 1][num + p]) \ = (cnt[r][num] + cnt[r][num + 1] + cdots + cnt[r][num + p]) - (cnt[l - 1][num] + cnt[l - 1][num + 1] + cdots + cnt[l - 1][num + p]) \ end{array}$

    不难发现:

    $cnt[r][num] + cnt[r][num + 1] + cdots + cnt[r][num + p]$ 对应的就是第 $r$ 棵线段树上区间 $[num,num+p]$ 的值;

    $cnt[l - 1][num] + cnt[l - 1][num + 1] + cdots + cnt[l - 1][num + p]$ 对应的就是第 $l-1$ 棵线段树上区间  $[num,num+p]$ 的值;

    这不就变成 $O(log n)$ 查询了嘛!舒服!

    所以最基础的主席树,解决的无非就是一个问题:

    对于一个数字串 $s$,我可以 $O(log n)$ 查询任意区间 $[l,r]$ 内任意连续若干个数字 $num,num+1,num+2,...,num+p$ 的出现次数之和。


    回归到本题,给出的数字序列是一个没有重复的整数序列 $s$,长度为 $n$,求任意区间内第 k 小元素,

    为了方便描述,不妨先用离散化处理该整数序列,得到离散化后值域应当为 $[1,n]$,那么原数字序列就变成 $(1,2,3,...,n)$ 的某一个排列,

    那么就可以用主席树,对查询函数稍作修改后,就能 $O(log n)$ 查询:

    任意区间 $[l,r]$ 内,我给定出现次数 $k$,我可以返回一个数 $p$,$p$ 满足数字 $1,2,...,p$ 在的区间 $[l,r]$ 内出现次数之和等于 $k$。

    AC代码:

    #include<cstdio>
    #include<algorithm>
    #include<vector>
    using namespace std;
    const int maxn=100000+5;
    
    //主席树
    struct Node{
        int l,r,sum;
    }node[maxn*40];
    int root[maxn];
    int cnt;
    void update(int l,int r,int x,int &y,int pos)
    {
        cnt++;
        node[cnt]=node[x];
        node[cnt].sum++;
        y=cnt;
    
        if(l==r) return;
    
        int mid=(l+r)/2;
        if(mid>=pos) update(l,mid,node[x].l,node[y].l,pos);
        else update(mid+1,r,node[x].r,node[y].r,pos);
    }
    int query(int l,int r,int x,int y,int k)
    {
        if(l==r) return l;
    
        int mid=(l+r)/2;
        int sum=node[node[y].l].sum-node[node[x].l].sum;
        if(sum>=k) return query(l,mid,node[x].l,node[y].l,k);
        else return query(mid+1,r,node[x].r,node[y].r,k-sum);
    }
    
    //离散化
    vector<int> v;
    inline int getID(int x){return lower_bound(v.begin(),v.end(),x)-v.begin()+1;}
    inline int getVal(int id){return v.at(id-1);}
    
    int a[maxn];
    int main()
    {
        int n,q;
        scanf("%d%d",&n,&q);
    
        v.clear();
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            v.push_back(a[i]);
        }
        sort(v.begin(),v.end());
        v.erase(unique(v.begin(),v.end()),v.end());
    
        root[0]=0; node[0].l=node[0].r=0; node[0].sum=0; //初始化第0棵树
        for(int i=1;i<=n;i++) update(1,n,root[i-1],root[i],getID(a[i])); //构建第1~n棵树
    
        for(int i=1;i<=q;i++)
        {
            int l,r,k;
            scanf("%d%d%d",&l,&r,&k);
            printf("%d
    ",getVal(query(1,n,root[l-1],root[r],k)));
        }
    }

    复杂度:

    空间复杂度:

      第0棵树$Oleft( n ight)$(其实实际代码写的时候是$Oleft( 1 ight)$),第1~n棵树每次需要$Oleft( {log n} ight)$,总的就是$Oleft( {nlog n} ight)$。

    时间复杂度:

      建树$Oleft( {nlog n} ight)$。

      一次查询$Oleft( {log n} ight)$。

    注:

    vector容器的方法:

      end()  指向迭代器中末端元素的下一个,指向一个不存在元素。
      erase(pos)  删除pos位置的数据,传回下一个数据的位置。
      erase(st,ed)  删除[st,ed)区间的数据,传回下一个数据的位置。

    unique函数:

      unique(st,ed)  对[st,ed)区间的数据进行去重。

      功能:对有序的容器重新排列,将所有第一次出现的元素从前往后排,所有重复出现的元素依次排在后面。

      返回值:返回迭代器,迭代器指向的是重复元素的首地址。

    lower_bound(st,ed,x)  在有序的序列中返回[st,ed)区间内第一个不小于x的元素的位置。

    upper_bound(st,ed,x)  在有序的序列中返回[st,ed)区间内第一个大于x的元素的位置。

  • 相关阅读:
    spark的做算子统计的Java代码(在Linux系统集群式运行)
    http协议面试题
    vue响应式原理
    vue-cli3搭建vue项目
    vscode中自定义代码片段
    vue中常用的全局配置
    tomcat安装配置
    Git相关
    nginx配置文件详解
    nginx源码安装
  • 原文地址:https://www.cnblogs.com/dilthey/p/9339863.html
Copyright © 2011-2022 走看看