题目背景
这是个非常经典的主席树入门题——静态区间第K小
数据已经过加强,请使用主席树。同时请注意常数优化
题目描述
如题,给定N个正整数构成的序列,将对于指定的闭区间查询其区间内的第K小值。
输入输出格式
输入格式:
第一行包含两个正整数N、M,分别表示序列的长度和查询的个数。
第二行包含N个正整数,表示这个序列各项的数字。
接下来M行每行包含三个整数 l, r, kl,r,k , 表示查询区间[l, r][l,r] 内的第k小值。
输出格式:
输出包含k行,每行1个正整数,依次表示每一次查询的结果
输入输出样例
输入样例#1:
5 5
25957 6405 15770 26287 26465
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1
输出样例#1:
6405
15770
26287
25957
26287
说明
数据范围:
对于20%的数据满足:\\(1 ≤ N, M \\leq 10\\)
对于50%的数据满足:\\(1 \\leq N, M \\leq 10^3\\)
对于80%的数据满足:\\(1 \\leq N, M \\leq 10^5\\)
对于100%的数据满足:\\(1 \\leq N, M \\leq 2*10^5\\)
对于数列中的所有数\\({a_i}\\),均满足\\(-10^9 \\leq a_i \\leq 10^9\\)
样例数据说明:
N=5,数列长度为5,数列从第一项开始依次为[25957, 6405, 15770, 26287, 26465 ][25957,6405,15770,26287,26465]
第一次查询为[2, 2][2,2] 区间内的第一小值,即为6405
第二次查询为[3, 4][3,4] 区间内的第一小值,即为15770
第三次查询为[4, 5][4,5] 区间内的第一小值,即为26287
第四次查询为[1, 2][1,2] 区间内的第二小值,即为25957
第五次查询为[4, 4][4,4] 区间内的第一小值,即为26287
贴一个巨佬同学的链接:k_lord
主席树是一个 利用线段树前缀和 的思想,所以在学这个之前最好先学习一下线段树.
既然要求第k大,那么我们先不考虑区间的问题,将所有的元素去重后排个序,那么第k个下标的元素就是第k大的值.
所以,我们先 将所有的元素去重排序后加入一个线段树.为了防止值过大,可以先将 数据离散化 一下.接下来按照原序列顺序,一个个加入线段树,从根节点开始往下直到找到第该元素排名个数个的线段树中节点的位置,将沿途经过的每个节点的记录的值+1.
听起来有点绕,我们这里借用一组数据模拟一下.假设现在有一个序列是1 5 2 6 3 7 4
一开始的一颗线段树中的所有节点记录的值都为0,我们把这个记录的值叫做经过这个节点的次数.
现在得到的所有叶子节点就是按顺序数过来的第1~k的元素(k是去重后元素个数).
然后按照原序列顺序向线段树中插入元素,第一个插入的元素是1,1是第一小的元素,则插入到最左边的叶子节点.路径上所有节点经过次数+1.
同理插入之后的元素.
直到最后一个元素插入完成:
这样插入完所有元素后, 经过第x次修改的线段树就可以查询序列第1 ~ x位中的第k大 了.
为什么这样记录出的线段树可以查询1 ~ x的第k大呢?我们想,现在已经记录了每个节点被经过的次数了,事实上可以理解为记录了每个节点的子树大小,因为第i个叶子节点代表就是第i小的值.那么那些存在于1 ~ x中的值已经被统计进来了,这样统计第k大的方式就类似于splay的统计第k大,如果k小于一个节点的左儿子的子树大小,则说明第k大存在于该节点的左子树中.否则则在右子树中. 因为实际有值的还是叶子节点,所以不用考虑第k大在非叶子节点上.
这一步可以通过上面的图以及自己出一些样例多理解一下.
然后再来考虑如何查询[l,r]区间的第k大.因为我们已经统计出了n颗线段树代表修改x次后的线段树的形态,而这些形态又都是相同的.所以我们可以用一个前缀和的思想,将查询[l,r]区间转变为查询一颗修改过 r 次的树减去一颗修改过 l-1 次的树,这样得到的一颗树表示的意义就可以理解为它是经过了[l,r]中元素修改的一棵树.那么再在这颗树上找第k大也是同理了.
因为我们要把每次修改的树都记录下来,但是很显然直接开n颗线段树是会爆空间的.所以我们可以将这些树没有被修改的部分共用节点,然后用数组记下每次修改后的线段树的根的编号.查询区间时就直接通过这两个根所包含的两颗线段树作差来求出一个查询该区间线段树.
举个栗子: 假设现在查询第2 ~ 5区间.
然后将对应位置的节点记录的值相减.这样得出来的树就是经过[2,5]的元素的修改的树了.懒得自己再去做图了....手画一下吧
下面看一下代码的注释吧.
#include<bits/stdc++.h>
using namespace std;
const int N=200000+5;
int n, m, size, cnt = 0;
int w[N];//用于记录原数组
int s[N];//用于记录去重排序后数组
int rk[N];//用于记录原数组的元素是第几小
int root[N];//用于记录每个根节点的编号
struct President_tree{//不要管什么总统树什么树的变量名啦
int ls, rs, sum;//ls,rs分别记录一个节点的左右儿子编号,sum记录经过该节点的次数
}t[N*20];
int gi(){//读入优化
int ans = 0 , f = 1; char i = getchar();
while(i<\'0\'||i>\'9\'){if(i==\'-\')f=-1;i=getchar();}
while(i>=\'0\'&&i<=\'9\'){ans=ans*10+i-\'0\';i=getchar();}
return ans * f;
}
void build(int &node,int l,int r){//建一颗空树(虽然不建也没什么关系)
node = ++cnt;
if(l == r) return;
int mid = (l+r>>1);
build(t[node].ls,l,mid);
build(t[node].rs,mid+1,r);
}
void updata(int &node,int last,int l,int r,int s){
node = ++cnt; t[node]=t[last]; ++t[node].sum;//通过上一次修改的值来修改
if(l == r) return;
int mid = (l+r>>1);
if(s <= mid) updata(t[node].ls,t[last].ls,l,mid,s);//如果最后要插到叶子节点的位置在mid左边,就往左插
else updata(t[node].rs,t[last].rs,mid+1,r,s);//同理
}
int query(int node,int last,int l,int r,int k){
if(l == r) return s[l];//查询到了叶子节点时,就找到了是在去重排序后序列中第L位置的值
int sum = t[t[node].ls].sum-t[t[last].ls].sum , mid = (l+r>>1);//类似于splay的查询第k大
if(k <= sum) return query(t[node].ls , t[last].ls , l , mid , k);
else return query(t[node].rs , t[last].rs , mid+1 , r , k-sum);
}
int main(){
int x, y, k; n = gi(); m = gi();
for(int i=1;i<=n;i++) w[i] = gi();
memcpy(s,w,sizeof(s));
sort(s+1,s+n+1);//排序
size = unique(s+1,s+n+1)-s-1;//unique可以将数组去重,因为unique返回的是一个地址,而数组以1开始,所以排序后的数组大小为这个值
build(root[0],1,size);
for(int i=1;i<=n;i++) rk[i] = lower_bound(s+1,s+size+1,w[i])-s;//处理每个数字是第几小,要在已经去重后的s数组中查找
for(int i=1;i<=n;i++)
updata(root[i],root[i-1],1,size,rk[i]);//将每次修改后的线段树存下来,以根节点来找整棵树
for(int i=1;i<=m;i++){
x = gi(); y = gi(); k = gi();
printf("%d\\n",query(root[y],root[x-1],1,size,k));
}
return 0;
}