终于学习了传说中的主席树,开森… …
前置知识
- 权值线段树
- 前缀和思想
思路
题意:给定一个长为n的数列
a
i
a_i
ai,m次询问,每次询问给定
l
,
r
,
k
l,r,k
l,r,k,求区间
[
l
,
r
]
[l,r]
[l,r]内的第k小数。
n
,
m
≤
2
e
5
n,m\le2e5
n,m≤2e5
首先,我们要离散化,因为
a
i
a_i
ai可能很大,但
n
n
n只有
2
e
5
2e5
2e5。
然后,我们可以枚举1~n,建 n n n棵权值线段树,表示 [ 1 , i ] [1,i] [1,i]区间内每个数出现的次数。
因为每棵树的节点数都等于离散化后
s
z
sz
sz的大小,所以每棵树是可以相减的。
于是,我们就可以很惊喜地发现:我们可以通过相减得到任意
[
l
,
r
]
[l,r]
[l,r]内的权值线段树!
这就是所谓的前缀和思想!
接下来,我们就要面对一个更严峻的问题:建
n
n
n个权值线段树时空复杂度都要炸!
这就来到了主席树的精髓(敲黑板)!!!
我们可以发现其实我们每建一个新的权值线段树,其中大部分节点都是不变的,最多只有
l
o
g
n
logn
logn个节点发生变化,因此,我们每次只需要暴力新建这
l
o
g
n
logn
logn个节点就OK了!
到此为止,一颗优秀的主席树就大功告成了!
最后就是如何查询。
这个问题在有了之前的前缀和思想以后就迎刃而解了。
结合二分,假设我们已经分别找到了第
r
r
r棵线段树和第
l
−
1
l-1
l−1棵线段树的
u
,
v
u,v
u,v节点,计算出此时
[
l
,
r
]
[l,r]
[l,r]中左儿子的
s
u
m
sum
sum,
c
n
t
=
t
r
[
t
r
[
u
]
.
l
]
.
s
u
m
−
t
r
[
t
r
[
v
]
.
l
]
.
s
u
m
cnt=tr[tr[u].l].sum-tr[tr[v].l].sum
cnt=tr[tr[u].l].sum−tr[tr[v].l].sum。判断
c
n
t
≥
k
cnt\ge k
cnt≥k,
如果是,就在
u
,
v
u,v
u,v的左儿子中继续二分;如果不是,就在
u
,
v
u,v
u,v的右儿子中寻找第
k
−
c
n
t
k-cnt
k−cnt大的数。
模板
#include<bits/stdc++.h>
using namespace std;
int read()
{
int i=0;char ch;
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) {i=i*10+ch-'0';ch=getchar();}
return i;
}
const int N=2e5+5;
struct node{
int l,r,sum;
}tr[N*20];
int n,m,l,r,p,tot,ans,a[N],b[N],rt[N];
void build(int y,int &x,int l,int r,int p)
{
tr[x=++tot]=tr[y];
++tr[x].sum;
if(l==r) return;
int mid=l+r>>1;
if(p<=mid) build(tr[y].l,tr[x].l,l,mid,p);
else build(tr[y].r,tr[x].r,mid+1,r,p);
}
int query(int y,int x,int l,int r,int k)
{
if(l==r) return l;
int s=tr[tr[x].l].sum-tr[tr[y].l].sum;
int mid=l+r>>1;
if(s>=k) return query(tr[y].l,tr[x].l,l,mid,k);
else return query(tr[y].r,tr[x].r,mid+1,r,k-s);
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) a[i]=b[i]=read();
sort(b+1,b+n+1);
int sz=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+sz+1,a[i])-b;
for(int i=1;i<=n;i++) build(rt[i-1],rt[i],1,sz,a[i]);
for(int i=1;i<=m;i++)
{
l=read();r=read();p=read();
ans=b[query(rt[l-1],rt[r],1,sz,p)];
printf("%d\n",ans);
}return 0;
}