正题
评测记录:https://www.luogu.org/recordnew/lists?uid=52918&pid=P3834
题意
给定一个长度为n的序列,有m个询问,求一个区间内的第k小的树。
解题思路
我们先思考用线段树快速询问第k小的树
我们可以用权值线段树来处理第k小的树,先将数组离散化(排序加去重),然后下标
表示在离散化序列内
中的数出现过多少次,然后因为已经离散化了所以我们可以直接根据两个子节点返回的值来进行向下寻找。
我们开始考虑区间问题。区间问题就是只算上这个区间内的数的线段树,我们可以使用前缀和。建立n个线段树表示
这个区间,然后每次查询就是用第
棵线段树减去·第
棵线段树就好了。
但是这么一算其实时间复杂度和空间复杂度还不如暴力那就直接暴力吧。这么一想我们会发现其实每次修改都只修改一条链,那么我们为什么不每次只增加一条链呢?
那么我们开始今天的正题:可持久化线段树(主席树)。
直接给出结构
(上面的数是区间范围,下面的是值。绿色为新增加的点)
我们每次只修改一条链,然后没有修改的那一部分和原来的节点相连,每次询问不同版本时只需要从不同版本的根节点开始访问就可以了。而这样我们就需要动态开点了。
代码
#include<cstdio>
#include<algorithm>
#define MN 200010
using namespace std;
struct tnode{
int w,l,r,ls,rs;
}t[MN<<5];
int n,m,x,y,k,a[MN],b[MN],root[MN],num,q,w;
int build(int l,int r)//建立一棵空树
{
int k=++num;
t[k].l=l,t[k].r=r;
if (l==r) return k;
int mid=(l+r)>>1;
t[k].ls=build(l,mid);
t[k].rs=build(mid+1,r);//左右分区
return k;//返回编号
}
int addt(int k,int z)//建立一条新链
{
int nb=++num;
t[nb]=t[k];t[nb].w++;//动态开点
if (t[k].l==z&&t[k].r==z) return nb;//到达节点
int mid=(t[k].l+t[k].r)>>1;
if (z<=mid) t[nb].ls=addt(t[k].ls,z);
else t[nb].rs=addt(t[k].rs,z);//建立下一个节点
return nb;
}
int query(int k1,int k2,int k)//前缀和询问区间
{
if (t[k1].l==t[k1].r) return t[k1].l;
w=t[t[k2].ls].w-t[t[k1].ls].w;//计算左子树在这个区间内的数字数量
if (k<=w) return query(t[k1].ls,t[k2].ls,k);
else return query(t[k1].rs,t[k2].rs,k-w);//向下询问
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]),b[i]=a[i];
sort(b+1,b+1+n);//排序(离散化1)
int q=unique(b+1,b+1+n)-b-1;//去重(离散化)
root[0]=build(1,q);//建立空树
for (int i=1;i<=n;i++)
{
int te=lower_bound(b+1,b+1+q,a[i])-b;//二分寻找原来的位置
root[i]=addt(root[i-1],te);//建立一条链
}
for (int i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&k);
printf("%d\n",b[query(root[x-1],root[y],k)]);//询问区间
}
}