题目背景
这是个非常经典的主席树入门题——静态区间第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 \leq N, M \leq 101≤N,M≤10
对于50%的数据满足:1 \leq N, M \leq 10^31≤N,M≤103
对于80%的数据满足:1 \leq N, M \leq 10^51≤N,M≤105
对于100%的数据满足:1 \leq N, M \leq 2\cdot 10^51≤N,M≤2⋅105
对于数列中的所有数a_iai,均满足-{10}^9 \leq a_i \leq {10}^9−109≤ai≤109
样例数据说明:
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小的数,我们通常用的普通的线段树每个结点只能保存一个数(往往这个数维护的是节点的数值),这样要找第k小的数的话就得建立多个线段树,很难实现,这就用到了主席树(或者说是可持久化权值线段树),权值线段树就是维护值域区间内每个数值的个数,这样便于实现找第k小的数的操作,在这之前如果数据很大要进行离散化(就是把这些数排序去重后得到每个数对应的相对位置(即下标)就是其离散化后的数值),主席树是一个可持久化权值线段树(也就是依次在原有的权值线段树的基础上插入新的数构成新的权值线段树(原有的其他结点不用变)),插入时按照原数列中数的顺序将数依次转化为离散化后对应的数进行插入,经过这样不断动态建树后,当前版本的树中数的个数都要加上前面版本的线段树中数的个数(类似于前缀和的思想:当前版本的树中数的个数=前面版本的线段树中数的个数+“1”(新插入的数的个数),因为是按照原数列中数的顺序将数依次转化为离散化后对应的数进行插入,所以每次可维护的区间长度就加一(左界不变,右界+1)),这样在查找区间[l,r]时,只需要用root[r]版本的线段树减去root[l-1]版本的线段树即可得到[l.r]这个区间内每个数的个数及状态,题目一开始的离散化复杂度为O(nlgn)O(nlgn),构建基础主席树复杂度为O(nlgn),统计并插入的复杂度O(nlgn+nlgn)=O(nlgn),询问的复杂度是O(mlgn)。复杂度总和就是O((m+n)lgn)。
完整代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+5;
int n,m,a[maxn];
vector<int> v;
inline int getid(int x)
{
return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}
typedef struct Node
{
int l,r,sum;
}no;
no hjt[maxn*40];
int cnt,root[maxn];
void insert(int l,int r,int pre,int &now,int p)
{
hjt[++cnt]=hjt[pre];
now=cnt;
hjt[now].sum++;
if(l==r) return;
int m=(l+r)>>1;
if(p<=m) insert(l,m,hjt[pre].l,hjt[now].l,p);
else insert(m+1,r,hjt[pre].r,hjt[now].r,p);
}
int query(int l,int r,int L,int R,int k)
{
if(l==r) return l;
int m=(l+r)>>1;
int t=hjt[hjt[R].l].sum-hjt[hjt[L].l].sum;
if(k<=t) return query(l,m,hjt[L].l,hjt[R].l,k);
else return query(m+1,r,hjt[L].r,hjt[R].r,k-t);
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
v.push_back(a[i]);
}
sort(v.begin(),v.end());
v.erase(std::unique(v.begin(),v.end()),v.end());
for(int i=1;i<=n;i++)
{
insert(1,n,root[i-1],root[i],getid(a[i]));
}
while(m--)
{
int l,r,k;
scanf("%lld%lld%lld",&l,&r,&k);
int id=query(1,n,root[l-1],root[r],k)-1;
int ans=v[id];
printf("%lld\n",ans);
}
return 0;
}
下面对代码进行一下简单的说明:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+5;
int n,m,a[maxn];
vector<int> v;
inline int getid(int x)//求离散化后的数值
{
return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}
typedef struct Node
{
int l,r,sum;
}no;
no hjt[maxn*40];//hjt[]用来给树的每个结点分配空间
int cnt,root[maxn];//root[i]的值是当前第i个版本树的根结点的编号
//插入函数
void insert(int l,int r,int pre,int &now,int p)//l,r代表当前区间,pre为上一个版本的权值线段树对应的位置,now为当前版本线段数对应的位置,p是要插入的数
{
hjt[++cnt]=hjt[pre];//直接复制一个上一个版本的权值线段树对应当前位置的结点
now=cnt;//更新编号
hjt[now].sum++;//当前结点的值的个数+1
if(l==r) return;
int m=(l+r)>>1;// >>1 == /2
if(p<=m) insert(l,m,hjt[pre].l,hjt[now].l,p);//p<=m表示当前编号在左子树里,把当前结点往左子树里插
else insert(m+1,r,hjt[pre].r,hjt[now].r,p);
}
int query(int l,int r,int L,int R,int k)//l,r为区间左右界,L是上个版本(l-1版本)的树的编号,R是当前版本(r版本)的树的编号,k是第k小的数,该函数返回的是从l到r这个区间内的第k小数的id(离散化后的值)
{
if(l==r) return l;//编号就对应离散化后的值,所以返回编号即可
int m=(l+r)>>1;
int t=hjt[hjt[R].l].sum-hjt[hjt[L].l].sum;//t计算当前结点左子树值的个数
if(k<=t) return query(l,m,hjt[L].l,hjt[R].l,k);//t<=t表示第k小在左子树
else return query(m+1,r,hjt[L].r,hjt[R].r,k-t);//因为是第k小,数值桉顺序从左子树排到右子树,所以k-t才是第k小在右子树中的位置
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
v.push_back(a[i]);
}
//接下来这两行为排序和去重操作
sort(v.begin(),v.end());
v.erase(std::unique(v.begin(),v.end()),v.end());
for(int i=1;i<=n;i++)
{
insert(1,n,root[i-1],root[i],getid(a[i]));//插入时按照原数列中数的顺序将数依次转化为离散化后对应的数进行插入
}
while(m--)
{
int l,r,k;
scanf("%lld%lld%lld",&l,&r,&k);
int id=query(1,n,root[l-1],root[r],k)-1;//查找区间[l,r]时,只需要用root[r]版本的线段树减去root[l-1]版本的线段树即可得到[l.r]这个区间内每个数的个数及状态
int ans=v[id];//还原成离散化之前最初的值
printf("%lld\n",ans);
}
return 0;
}