(菜鸡这两天学了静态主席树,看了好多大佬的代码和解释= =,终于看懂了!在此写一个菜鸡详细版的静态主席树总结O(∩_∩)O~~)
介绍:
主席树也称函数式线段树也称可持久化线段树。
作用:
求任意区间[L,R]的第k大数。假设这个区间是[1,R]:
(1)我们若每次求区间[1,R]的第k大数,则用权值线段树可求解,即从根节点开始向下递归,若左子节点的sum值>=k,可见第k大的值在左子树里,向左递归;反之,向右递归,直到当前区间l==r时,l即为所求值。
比如,插入数:1 2 2 4 3 5 ,求第3大的数(图中区间旁标的是线段树的sum值)
(2)若是求任意区间的该怎么办呢,静态主席树登场~~~
原理:
所谓主席树呢,就是对原来的数列[1..n]的每一个前缀[1..i](1≤i≤n)建立一棵线段树,线段树的每一个节点存某个前缀[1..i]中属于区间[L..R]的数一共有多少个,如下图:
比如,插入数:1 10 4 2
(1)去重,离散化:1 2 3 4,相当于依次插入点1 4 3 2
(2)对于数列1到4,构建5棵前缀线段树,如图:
rt[i]表示第i棵树的根节点编号;
红色圈圈表示节点编号;
绿色笔标注的是个节点sum值;
铅笔箭头表示更新的情况;
(3)查找,若要查找[i..j]中第k大数时,则让第j棵树,减第i-1棵树,此时的得到的新树,记录的正好是区间[l,r]的情况,在得到的树中查找第k大的数即可,
比如说,区间[2,4]中第2大,则树4减树1得到:
[1,4](3)
[1,2] (1) [2,4](2)
[1,1] [2,2] (1) [3,3](1) [4,4](1)
在这棵树上查找第2的的数,为3
(4)存在问题:对每一个前缀都建一棵树,会MLE,观察到每个[1..i]和[1..i-1]只有一条路是不一样的(铅笔标出的更新的路径),那么其他的结点只要用回前一棵树的结点即可,时空复杂度为O(nlogn)。
那么,每棵树都重复利用前一棵树的节点,可达到节省节点(节省空间的)效果,当然,每棵树根节点编号会发生改变:
代码实现:
知道了原理,代码就好说多了~
(1)输入数组,去重,离散化(不多说)
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(a+1,a+1+n);
int nn=unique(a+1,a+1+n)-(a+1);
(2)初始化第0棵树
通过上面的“原理部分”的图,大家可能发现了,主席树在建树的时候,和普通线段树的建树操作不太一样。
对于普通线段树:根节点的编号是x,左子树的编号是x<<1,右子树是x<<1|1;
而对于主席树,由于多个节点重复使用,很显然不满足这个性质,所以,要用rt[i],ls[rt[i]],rs[rt[i]]分别存,树i的根节点编号,左子树根节点编号,右子树根节点编号。
我们用递归的方法,根节点编号为x,则左子树根节点为x+1,
再以左子树为根节点,递归其左子树,直到左子树为空,再去递归右子树,(相当于先序遍历,可结合上图理解一下主席树编号是怎么赋值的)
rt[i]:=树i的根节点编号是多少
ls[rt[i]]:=根节点编号为rt[i]的左子树的根节点编号
rs[rt[i]]:=根节点编号为rt[i]的右子树的根节点编号
//build(rt[0],1,nn);
void build(int &o,int l,int r){
o=++tot;
sum[o]=0;
if(l==r)return ;
int m=(l+r)>>1;
build(ls[o],l,m);
build(rs[o],m+1,r);
}
(3)从1到n建树操作
由“原理”部分可知,插入i节点的时候,建第i棵树,是在第i-1棵树的基础上建的,
先把新树,左子树的 ls 和 rs 都赋值成老树的 ls 和 rs,当然,其中一个必定会改变(每次更新只改变一条路径嘛)
更新的点p在其左子树中时,左节点肯定要变啦,右节点不变,直接用之前的节点就好啦!(rs[o]=rs[pre])
此时update(ls[o],l,m,ls[pre],p);
改变左子树的ls的值,和sum值
/*
for(int i=1;i<=n;i++){
int x=lower_bound(a+1,a+1+nn,b[i])-a;
update(rt[i],1,nn,rt[i-1],x);
}
*/
void update(int &o,int l,int r,int pre,int p){
o=++tot;
ls[o]=ls[pre];
rs[o]=rs[pre];
sum[o]=sum[pre]+1;//节点新插入一个元素,sum值比老sum值多1
if(l==r)return ;
int m=(l+r)>>1;
if(p<=m)update(ls[o],l,m,ls[pre],p);
else update(rs[o],m+1,r,rs[pre],p);
}
(4)查询
假设查询区间[l,r],则让第r棵树-第(l-1)棵树,在得到的这棵新树上查询就好啦
第r棵树根节点编号:rr=rt[r]
第r棵树左子树根节点的编号:ls[rr]
第r棵树左子树的sum值:sum[ls[rr]]
同理,第l-1棵树,左子树的sum值:sum[ls[lr]]
cnt=sum[ls[rr]]-sum[ls[lr]] := 第r棵树-第(l-1)棵树的左子树的sum值
若k<=cnt,查询左子树,
反之,查询右子树
//int ans=query(rt[l-1],rt[r],1,nn,k);
int query(int lr,int rr,int l,int r,int k){
if(l==r)return l;
int m=(l+r)>>1;
int cnt=sum[ls[rr]]-sum[ls[lr]];
if(k<=cnt)return query(ls[lr],ls[rr],l,m,k);
else return query(rs[lr],rs[rr],m+1,r,k-cnt);
}
完结撒花✿✿ヽ(°▽°)ノ✿
看一道模板题:
代码如下:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<queue>
#include<stack>
#include<cmath>
#include<set>
#include<map>
using namespace std;
#define ll long long
typedef pair<int,int>P;
const int INF=0x3f3f3f3f;
const int N=200005;
int a[N],b[N],tot=0;
int rt[N],ls[N*20],rs[N*20],sum[N*20];
void build(int &o,int l,int r){
o=++tot;
sum[o]=0;
if(l==r)return ;
int m=(l+r)>>1;
build(ls[o],l,m);
build(rs[o],m+1,r);
}
void update(int &o,int l,int r,int pre,int p){
o=++tot;
ls[o]=ls[pre];
rs[o]=rs[pre];
sum[o]=sum[pre]+1;
if(l==r)return ;
int m=(l+r)>>1;
if(p<=m)update(ls[o],l,m,ls[pre],p);
else update(rs[o],m+1,r,rs[pre],p);
}
int query(int lr,int rr,int l,int r,int k){
if(l==r)return l;
int m=(l+r)>>1;
int cnt=sum[ls[rr]]-sum[ls[lr]];
if(k<=cnt)return query(ls[lr],ls[rr],l,m,k);
else return query(rs[lr],rs[rr],m+1,r,k-cnt);
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(a+1,a+1+n);
int nn=unique(a+1,a+1+n)-(a+1);
int l,r,k;
build(rt[0],1,nn);
for(int i=1;i<=n;i++){
int x=lower_bound(a+1,a+1+nn,b[i])-a;
update(rt[i],1,nn,rt[i-1],x);
}
while(m--){
scanf("%d%d%d",&l,&r,&k);
int ans=query(rt[l-1],rt[r],1,nn,k);
printf("%d\n",a[ans]);
}
}