主席树 学习笔记

主席树

主席树又叫可持久化线段树,就是可以提取历史版本的线段树。
主席树的一个基本操作就是求解区间第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大就可以了.
这里写图片描述

代码

提交地址:
洛谷
POJ

#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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值