主席树
主席树又叫可持久化线段树,就是可以提取历史版本的线段树。
主席树的一个基本操作就是求解区间第K大。
原理
主席树的原理其实很好理解。
假如我们有一个数列
(3,2,3,1,4)
(
3
,
2
,
3
,
1
,
4
)
首先要离散化.
我们要对这一个数列建一个线段树。
这一个线段树,最底层每一个节点代表这一节点的下标在数列中出现的次数,父亲就是就和就可以,因为离散化了,所以空间也不会太大.
这一个是这个数列全部建完之后的线段树。
现在模拟一下查询的过程,假如我们要查询
[1,5]
[
1
,
5
]
第3大
.
过程就是:
先查看当前节点的左子树,如果左子树的权值小于k那么走到右子树查询
k−左子树的权值
k
−
左
子
树
的
权
值
,否则走到当前节点的左子树,查询k.
我们想一下如何求解区间第K大。
我们可以对每一个点建立一颗上面那样的线段树。
然后我们发现每一个点对于数列中的上一个点,只会改变一条链,所以我们可以利用上一个节点建立的线段树在上面做一些小小的修改
(logn)
(
l
o
g
n
)
就可以了,这也是主席树的核心思想.
我们可以利用这一个性质来进行优化,因为每一次只改变一个节点,改变的路径也就是从最底层改变的节点到根节点的路径,每一次是
logn
l
o
g
n
次.
对于数列中的每一个节点都建立一颗线段树后,我们发现对于这几颗线段树是具有差分的性质的。
比如说我们要求
[2,5]
[
2
,
5
]
的第2大,我们可以把这两个节点的线段树进行差分,得到新的一个线段树,然后在新的线段树上进行查询k大就可以了.
代码
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
const int MAXN=1e7;
using namespace std;
struct num
{
int rank,x;
}a[MAXN];
struct Node
{
int sum,l,r;
}Pri[MAXN*2];
int Pri_Num,x,y,z;
int root[MAXN];
void Insert(int num,int &now,int l,int r)
{
Pri[++Pri_Num]=Pri[now];
now=Pri_Num;
Pri[now].sum++;
if(l==r) return ;
int mid=(l+r)>>1;
if(num<=mid)
Insert(num,Pri[now].l,l,mid);
else
Insert(num,Pri[now].r,mid+1,r);
}
int query(int i,int j,int k,int l,int r)
{
if(l==r) return l;
int ans=Pri[Pri[j].l].sum-Pri[Pri[i].l].sum;
int mid=(l+r)>>1;
if(k<=ans)
return query(Pri[i].l,Pri[j].l,k,l,mid);
else
return query(Pri[i].r,Pri[j].r,k-ans,mid+1,r);
}
bool cmp(num a,num b)
{
return a.x<b.x;
}
int n,m,rank[MAXN];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i].x);
a[i].rank=i;
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
rank[a[i].rank]=i;//离散化
for(int i=1;i<=n;i++)
{
root[i]=root[i-1];//借助上一个节点的线段树
Insert(rank[i],root[i],1,n);
}
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
printf("%d\n",a[query(root[x-1],root[y],z,1,n)].x);//差分查询
}
return 0;
}