本文章大部分内容来自 Menci
主席树是一种数据结构,其主要应用是区间第 k 大问题。
权值线段树
传统的线段树用于维护一条线段上的区间,可以方便地查询区间信息。而如果将线段树转化为『权值线段树』,每个叶子节点存储某个元素出现次数,一条线段的总和表示区间内所有数出现次数的总和。
利用权值线段树可以方便地求出整体第
查找过程类似平衡树,时间复杂度为 O(logn) 。
前缀和
上述算法可以用来处理整个序列上的第
k
大,而我们可以对于一个长度为
这个算法存在两个问题:
每个线段树要占用
logn),占用空间过多;
建立每棵线段树至少要用
O(nlogn)
的时间,每次查询又要用
O(nlogn)
的时间构建区间的权值线段树,总时间复杂度
O((n+m)nlogn)
。
看上去还不如每次直接提取出区间,并使用后线性选择得到答案的
O(n2)
) 的朴素算法优秀。
主席树
仔细思考,发现上述算法的
n
棵线段树中,相邻的两棵线段树仅有
为了节省空间,可以将第
0
棵线段树置为空,每次插入一个新叶子节点时接入一条长度为
查询时构造整棵线段树,需要构造 O(nlogn) 个节点,但每次查询只会用到 O(logn) 个节点,直接动态构造这些节点即可。为了方便,可以不显式构造这些节点,而是直接用两棵线段树上的值相减。
模板
题目 K-th Number
#include<cstdio>
#include<vector>
#include<algorithm>
#define MAXN 100005
using namespace std;
int n,m,cnt,root[MAXN],a[MAXN],x,y,k;
struct node{int l,r,sum;}T[MAXN*40];
vector<int> v;
int getid(int x){return lower_bound(v.begin(),v.end(),x)-v.begin()+1;}
void update(int l,int r,int &x,int y,int pos)
{
T[++cnt]=T[y],T[cnt].sum++,x=cnt;
if(l==r) return ;
int mid=l+r>>1;
if(mid>=pos) update(l,mid,T[x].l,T[y].l,pos);
else update(mid+1,r,T[x].r,T[y].r,pos);
}
int query(int l,int r,int x,int y,int k)
{
if(l==r) return l;
int mid=l+r>>1;
int sum=T[T[y].l].sum-T[T[x].l].sum;
if(sum>=k) return query(l,mid,T[x].l,T[y].l,k);
else return query(mid+1,r,T[x].r,T[y].r,k-sum);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),v.push_back(a[i]);
sort(v.begin(),v.end());
v.erase(unique(v.begin(),v.end()),v.end());
for(int i=1;i<=n;i++) update(1,n,root[i],root[i-1],getid(a[i]));
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&k);
printf("%d\n",v[query(1,n,root[x-1],root[y],k)-1]);
}
return 0;
}
参考资料