主席树 学习笔记
对于询问$[1,n]$的第$k$小数,我们都知道直接上权值线段树就行了。那么对于任意区间的第$k$小数呢?
暴力一点,每次开一颗线段树。空间肯定爆炸。那么此时,主席树便应运而生。
主席树的主要思想就是:保留每次插入操作时的历史版本,以便查询区间第$k$小的数。先说流程。
1.先建一颗空的权值线段树,$[1,len]$。
2.从$1$到$n$对于每个结点都新建一颗权值线段树,$i$结点的线段树根据$i-1$的更新,即在原基础上进行加的操作。
3.若查询$[l,r]$则拿出第$l-1$颗和第$r$颗线段树进行比较,他们之间的差值就是$[l,r]$区间的元素个数。查找第$k$小,就看左儿子的大小$x$。如果$k\leq x$,那么答案肯定在左儿子,反之则在右儿子。注意此时$k$要更新成$k-x$,因为在右儿子的区间里相对大小会发生变化。
主席树有着满足前缀和和树上差分等优秀性质(感性理解),所以不管是树还是序列都可以维护。
注意:使用主席树时请不要吝啬你的空间,不然会出现奇奇怪怪的错误。一般来说我都开$n\log n$,实际上开$8*10^6$都可以。
更多内容详见OI Wiki。
代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int maxn=5000005; int n,m,len,a[maxn],b[maxn],sum[maxn],ls[maxn],rs[maxn],tot,rt[maxn]; inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline int getpos(int x){return lower_bound(b+1,b+len+1,x)-b;} inline int build(int l,int r) { int root=++tot; if (l==r) return root; int mid=(l+r)>>1; ls[root]=build(l,mid); rs[root]=build(mid+1,r); return root; } inline int update(int k,int l,int r,int root) { int dir=++tot; ls[dir]=ls[root],rs[dir]=rs[root];sum[dir]=sum[root]+1; int mid=(l+r)>>1; if (l<r) { if (k<=mid) ls[dir]=update(k,l,mid,ls[root]); else rs[dir]=update(k,mid+1,r,rs[root]); } return dir; } inline int query(int u,int v,int l,int r,int k) { if (l==r) return l; int x=sum[ls[v]]-sum[ls[u]],mid=(l+r)>>1; if (k<=x) return query(ls[u],ls[v],l,mid,k); else return query(rs[u],rs[v],mid+1,r,k-x); } signed main() { n=read(),m=read(); for (int i=1;i<=n;i++) a[i]=read(),b[i]=a[i]; sort(b+1,b+n+1); len=unique(b+1,b+n+1)-b-1; rt[0]=build(1,len); for (int i=1;i<=n;i++) rt[i]=update(getpos(a[i]),1,len,rt[i-1]); while(m--) { int l=read(),r=read(),k=read(); printf("%lld\n",b[query(rt[l-1],rt[r],1,len,k)]); } return 0; }