主席树,据说是某大神在考场忘记归并树怎么写,然后发明了%%%%%
Q:主席树经典操作—求取静态区间第k小
即给定一个长度为n的数列,每次询问一个区间内的第k小数值
A:主席树的名字太高大上
我们给他取个通俗点的名字—前缀线段树
为什么这么说呢
因为在静态区间第k小问题中
我们要建立n棵主席树
第i棵主席树维护的是数列中区间[1,i]每个数出现了多少次
也就是相当于对数列每个前缀建立了一个线段树统计前缀中数字出现次数
那么对于一个ll到rr区间的询问
我们用第rr棵主席树减去第ll-1棵主席树,就得到了所求区间
光这么说可能有些难以理解
来看看下面的例子
有n=5的数列,5 1 2 4 3
现在我们来建立主席树
按前缀顺序依次建立五棵主席树
其中黑色数字代表数值区间(注意不是序列编号)
红色数字是该区间内数值出现次数
假设现在询问2-4区间的第2小
那么我们取出第1(ll-1)棵和第4(rr)棵主席树
因为是第k小,所以先用根的左区间(1~3)相减
发现等于2(即数列24区间内数值13出现次数等于2)
说明第2小必定是1-3之内的数值
所以往左子树走
重复上述步骤
发现左子树相减还是等于2
说明第2小必定是1~2之内的数值
往左子树走
再次左子树相减
发现等于1
说明第2小不是1~1区间内的数值
往右走并令k-=左子树的差
走到叶子节点直接返回该点保存的数值
即第2~4区间第2小就是2
以下是查询代码
int get(int u,int v,int ll,int rr,int k)//u是第ll-1棵主席树,v是第rr棵,ll与rr是查询区间
{
if(ll==rr) return ll;//叶子节点直接返回数值
int x=sum[lft[v]]-sum[lft[u]];//左子树做差得左边区间内数值出现次数
int mid=ll+rr>>1;
if(x>=k) return get(lft[u],lft[v],ll,mid,k);//第k小在左边数值区间
else return get(rht[u],rht[v],mid+1,rr,k-x);//记得k-=x
}
虽然解决了询问
但还有一个棘手的问题
直接建立n棵主席树需要
的空间
显然无法承受
仔细观察可以发现
线段树每插入一个数值
只用修改原树中一条从根到叶子的路径
既然只有
个节点要修改
那为什么还要重新建树呢
所以我们直接把未修改的结点指针指回自己的上一棵主席树
对于做了修改的新建logn个结点
建完所有主席树所需空间大约为
建树代码
int update(int pre,int ll,int rr,int x)
{
int rt=++tot; sum[rt]=sum[pre]+1;//修改到的结点sum要增加
lft[rt]=lft[pre]; rht[rt]=rht[pre]; //先将左右子树指针指回上一个主席树
int mid=ll+rr>>1;
if(ll<rr)
{
//对要修改的结点新建
if(x<=mid) lft[rt]=update(lft[pre],ll,mid,x);
else rht[rt]=update(rht[pre],mid+1,rr,x);
}
return rt;
}
for(int i=1;i<=n;++i)
tree[i]=update(tree[i-1],1,n,read());//建立第i棵主席树
静态区间Kth果题
P3834 【模板】可持久化线段树 1(主席树)
由于题目中数值区间较大 ,所以需要离散化
#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
void print(int x)
{
if(x<0){putchar('-');x=-x;}
if(x>9)print(x/10);
putchar(x%10+'0');
}
int n,m;
int a[5000010],b[5000010];
int cnt,pos[5000010];
int tot,tree[5000010];
int sum[5000010];
int lft[5000010],rht[5000010];
int update(int pre,int ll,int rr,int x)
{
int rt=++tot; sum[rt]=sum[pre]+1;//修搞到的结点sum要更新
lft[rt]=lft[pre]; rht[rt]=rht[pre];//先指回上一棵主席树
int mid=ll+rr>>1;
if(ll<rr)
{
//有更新的结点新建
if(x<=mid) lft[rt]=update(lft[pre],ll,mid,x);
else rht[rt]=update(rht[pre],mid+1,rr,x);
}
return rt;
}
int get(int u,int v,int ll,int rr,int k)
{
if(ll==rr) return ll;
int x=sum[lft[v]]-sum[lft[u]];//左子树做差得左区间数值出现次数
int mid=ll+rr>>1;
if(x>=k) return get(lft[u],lft[v],ll,mid,k);//第k小在左数值区间
else return get(rht[u],rht[v],mid+1,rr,k-x);//记得k-=x
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
a[i]=read(),b[i]=a[i];
sort(b+1,b+1+n);//离散化
for(int i=1;i<=n;i++)
if(i==1||b[i]!=b[i-1])
pos[++cnt]=b[i];
for(int i=1;i<=n;i++)
{
int x=lower_bound(pos+1,pos+1+cnt,a[i])-pos;//查询离散化后的数值
tree[i]=update(tree[i-1],1,cnt,x);//建树
}
while(m--)
{
int ll=read(),rr=read(),k=read();
int num=get(tree[ll-1],tree[rr],1,cnt,k);
print(pos[num]);printf("\n");
}
return 0;
}