题目链接:http://poj.org/problem?id=2104
Description
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 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
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
题意:
写一个数据结构,能够快速返回指定区间内的第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容器的方法:
unique函数:
unique(st,ed) 对[st,ed)区间的数据进行去重。
功能:对有序的容器重新排列,将所有第一次出现的元素从前往后排,所有重复出现的元素依次排在后面。
返回值:返回迭代器,迭代器指向的是重复元素的首地址。
lower_bound(st,ed,x) 在有序的序列中,返回[st,ed)区间内第一个不小于x的元素的位置。
upper_bound(st,ed,x) 在有序的序列中,返回[st,ed)区间内第一个大于x的元素的位置。