【模板主席树】洛谷p3834

本文详细介绍了主席树的概念、原理及实现方式,并通过一道经典题目展示了如何利用主席树解决静态区间第K小值的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

主席树

主席树的主体是线段树,准确的说,是很多棵线段树,存的是一段数字区间出现次数(所以要先离散化可能出现的数字)。举个例子,假设我每次都要求整个序列内的第 k 小,那么对整个序列构造一个线段树,然后在线段树上不断找第 k 小在当前数字区间的左半部分还是右半部分。这个操作和平衡树的 Rank 操作一样,只是这里将离散的数字搞成了连续的数字。

先假设没有修改操作:

对于每个前缀 S1…i,保存这样一个线段树 Ti,组成主席树。这样是会 MLE 的,所以我们用到了一点优化。
注意,这个线段树对一条线段,保存的是这个数字区间的出现次数,所以是可以互相加减的!还有,由于每棵线段树都要保存同样的数字,所以它们的大小、形态也都是一样的!这实在是两个非常好的性质,是平衡树所不具备的。
对于询问 (i,j),我只要拿出 Tj 和 Ti-1,对每个节点相减就可以了。说的通俗一点,询问 i..j 区间中,一个数字区间的出现次数时,就是这些数字在 Tj 中出现的次数减去在 Ti-1 中出现的次数。

那么有修改操作怎么办呢?

如果将询问看成求一段序列的数字和,那么上面那个相当于求出了前缀和。加入修改操作后,就要用树状数组等来维护前缀和了。于是那个 “很好的性质” 又一次发挥了作用,由于主席树可以互相加减,所以可以用树状数组来套上它。做法和维护前缀和长得基本一样.

主席树中有一些性质方便我们进行运算

①主席树的每个结点,保存的是这个区间含有的数字的个数。
②主席树的每个结点,也就是每颗线段树的大小和形态也是一样的,也就是主席树之间可以相互进行加减运算。

……………………………………………………………………………………………………………………………………………………
然后还是看看这道模板题吧

题目背景

这是个非常经典的主席树入门题——静态区间第K小

题目描述

如题,给定N个正整数构成的序列,将对于指定的闭区间查询其区间内的第K小值。

输入格式:

第一行包含两个正整数N、M,分别表示序列的长度和查询的个数。
第二行包含N个正整数,表示这个序列各项的数字。
接下来M行每行包含三个整数 l, r, kl,r,k , 表示查询区间[l, r][l,r]内的第k小值。

输出格式:

输出包含k行,每行1个正整数,依次表示每一次查询的结果

输入样例:

5 5
25957 6405 15770 26287 26465
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1

输出样例

6405
15770
26287
25957
26287

数据范围:

对于20%的数据满足:1≤N,M≤10
对于50%的数据满足:1N,M103
对于80%的数据满足:1N,M105
对于100%的数据满足: 1N,M2105
对于数列中的所有数ai,均满足109ai109

思路

这题其实思路就非常明确了
就是主席树
首先将值离散化之后,构建一颗值域线段树储存区间和
初始的线段树是空树
每次在值域上增加1就重构一颗线段树
很显然,任意两颗相邻线段树的值得和差为1
而相同的区间内要么相等要么多1
那么,我们也很容易的可以推出,区间第k大可以通过第r版本和第(l-1)版本的线段树算出来
每次计算左儿子
如果r的左儿子已经比(l-1)的左儿子多出来的和大于k
那么在左儿子查找k大值
否则则在右儿子上查找第(k-val)大值,其中,val是和的差

代码

#include <bits/stdc++.h>
using namespace std;
inline int read(){
    int ret=0,f=1;char c=getchar();
    for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
    for(;isdigit(c);c=getchar())ret=ret*10+c-'0';
    return ret*f;
}
struct pppp{int l,r,sum,ls,rs;}tr[20000001];
int a[200005],b[200005],ls[200005];
int n,m,lsh[200005],pp=0,root[200005];
void build(int rt,int l,int r){
    ++pp;tr[rt].l=l;tr[rt].r=r;
    if(l==r)return ;
    int mid=(l+r)>>1;
    tr[rt].ls=pp+1;
    build(pp+1,l,mid);
    tr[rt].rs=pp+1;
    build(pp+1,mid+1,r);
}
void cr(int rt,int k){  
    ++pp;int mid=(tr[rt].l+tr[rt].r)>>1;
    tr[pp]=tr[rt];++tr[pp].sum;
    if(tr[rt].l==tr[rt].r)return ;
    if(k<=mid){tr[pp].ls=pp+1;cr(tr[rt].ls,k);}
    else{tr[pp].rs=pp+1;cr(tr[rt].rs,k);}
}
int ch(int x,int y,int k){
    if(tr[x].l==tr[x].r)return tr[x].l;
    int tmp=tr[tr[y].ls].sum-tr[tr[x].ls].sum;
    if(tmp>=k)return ch(tr[x].ls,tr[y].ls,k);
    else return ch(tr[x].rs,tr[y].rs,k-tmp);
}
int main(){int x,y,z;
    n=read();m=read();
    for(int i=1;i<=n;++i)b[i]=read(),a[i]=b[i];
    sort(b+1,b+n+1);
    int nn=unique(b+1,b+n+1)-b-1;
    for(int i=1;i<=nn;++i)lsh[i]=b[i],ls[b[i]]=i;
    build(1,1,nn);
    root[0]=1;
    for(int i=1;i<=n;++i){
        root[i]=pp+1;
        cr(root[i-1],ls[a[i]]);
    }
    while(m--){
        x=read();y=read();z=read();
        printf("%d\n",lsh[ch(root[x-1],root[y],z)]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值