传统线段树都是通过\(cur<<1\)和\(cur<<1|1\)分别访问左右儿子,而非堆式建树使用\(L[]\)和\(R[]\)访问左右儿子,儿子节点编号动态使用\(++topt\)分配(如分配\(cur\)新的一个左二子节点:L[cur] = ++topt;
)
主席树为了节省空间,就是通过这种建树方式建树的
void build(int &cur, ...){
if(cur==0)
cur = ++topt;
//...
}
主席树为每一个历史节点都新建了一棵线段树以供随时查询历史版本,但是除了第一棵是完全二叉树,其余都是不完全的线段树(如果每个历史节点全部新建就会MLE),主席树会共享没有改变过的节点。
记得存下每个历史节点的线段树的根节点以供访问。
题面,题意摘要:给定序列,每次询问区间\([l,r]\)的的第\(k\)小值
考虑依次对插入的数,每个数都建一棵值域线段树,树节点维护一个区间\([a,b]\),即在第\(a\)小到第\(b\)小范围内目前数的个数。依输入顺序插入数,并查询其排名(所以最好离散化一下),在这个当前新树下,将所有包含这个数的区间都+1
查询区间\([l,r]\)时,用前缀和思想,将第\(r\)个数的线段树减去第\(l-1\)个数,得出一棵包含区间\([l,r]\)内各数分布情况的树,再像Splay那样,每次查询小于当前范围数的个数,如果大于\(k\),则第\(k\)个数一定在前面那个范围里,反之,则在后面那个区间里。
关于空间复杂度,除了第一棵最大\(4n\)个节点,其余线段树每次只会最多新建\(log^n_2\)个节点,总空间复杂度为\(4n+(n-1)log^n_2\)(相对于原来的\(4n^2\))
AC Code
#include <cstdio>
#include <algorithm>
#define MAXN 200010
#define MAXM MAXN*20
using namespace std;
int n,q,sz,L[MAXM],R[MAXM],val[MAXM];
int a[MAXN],s[MAXM],his[MAXN];
int topt;
int query(int a, int b, int l, int r, const int k){
if(l==r) return l;
int sz = val[L[b]]-val[L[a]];
int mid = (l+r)>>1;
if(sz>=k) return query(L[a], L[b], l, mid, k);
else return query(R[a], R[b], mid+1, r, k-sz);
}
void add(int &cur, const int pre, int l, int r, int x){
cur=++topt;
L[cur]=L[pre], R[cur]=R[pre],val[cur]=val[pre]+1;
if(l==r) return;
int mid=(l+r)>>1;
if(x<=mid) add(L[cur], L[pre], l, mid, x);
else add(R[cur], R[pre], mid+1, r, x);
}
void buildt(int &cur, int l, int r){
cur=++topt;
if(l<r){
int mid = (l+r)>>1;
buildt(L[cur], l, mid);
buildt(R[cur], mid+1, r);
}
}
int main(){
scanf("%d %d", &n, &q);
for(register int i=1;i<=n;++i)
scanf("%d", &a[i]), s[i]=a[i];
sort(s+1, s+1+n);
sz = unique(s+1, s+1+n) - (s+1);
buildt(his[0], 1, sz);
for(register int i=1;i<=n;++i){
int t = lower_bound(s+1, s+1+sz, a[i]) - s;
add(his[i], his[i-1], 1, sz, t);
}
while(q--){
int l,r,k;
scanf("%d %d %d", &l, &r, &k);
printf("%d\n", s[query(his[l-1], his[r], 1, sz, k)]);
}
return 0;
}
原文:https://www.cnblogs.com/santiego/p/10799957.html