1. 程式人生 > 實用技巧 >主席樹 學習筆記

主席樹 學習筆記

對於詢問$[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; }