静态主席树学习总结(详细)

(菜鸡这两天学了静态主席树,看了好多大佬的代码和解释= =,终于看懂了!在此写一个菜鸡详细版的静态主席树总结O(∩_∩)O~~)

介绍:

主席树也称函数式线段树也称可持久化线段树。

作用:

求任意区间[L,R]的第k大数。假设这个区间是[1,R]:

(1)我们若每次求区间[1,R]的第k大数,则用权值线段树可求解,即从根节点开始向下递归,若左子节点的sum值>=k,可见第k大的值在左子树里,向左递归;反之,向右递归,直到当前区间l==r时,l即为所求值。

比如,插入数:1 2 2 4 3 5 ,求第3大的数(图中区间旁标的是线段树的sum值)

(2)若是求任意区间的该怎么办呢,静态主席树登场~~~

原理:

所谓主席树呢,就是对原来的数列[1..n]的每一个前缀[1..i](1≤i≤n)建立一棵线段树,线段树的每一个节点存某个前缀[1..i]中属于区间[L..R]的数一共有多少个,如下图:

比如,插入数:1 10 4 2

(1)去重,离散化:1 2 3 4,相当于依次插入点1 4 3 2

(2)对于数列1到4,构建5棵前缀线段树,如图:

rt[i]表示第i棵树的根节点编号;

红色圈圈表示节点编号;

绿色笔标注的是个节点sum值;

铅笔箭头表示更新的情况;

(3)查找,若要查找[i..j]中第k大数时,则让第j棵树,减第i-1棵树,此时的得到的新树,记录的正好是区间[l,r]的情况,在得到的树中查找第k大的数即可,

比如说,区间[2,4]中第2大,则树4减树1得到:

                     [1,4](3)

        [1,2] (1)                   [2,4](2)

[1,1]       [2,2] (1)      [3,3](1)        [4,4](1)

在这棵树上查找第2的的数,为3

(4)存在问题:对每一个前缀都建一棵树,会MLE,观察到每个[1..i]和[1..i-1]只有一条路是不一样的(铅笔标出的更新的路径),那么其他的结点只要用回前一棵树的结点即可,时空复杂度为O(nlogn)。

那么,每棵树都重复利用前一棵树的节点,可达到节省节点(节省空间的)效果,当然,每棵树根节点编号会发生改变:

代码实现:

知道了原理,代码就好说多了~

(1)输入数组,去重,离散化(不多说)

for(int i=1;i<=n;i++){
    scanf("%d",&a[i]);
    b[i]=a[i];
}
sort(a+1,a+1+n);
int nn=unique(a+1,a+1+n)-(a+1);

(2)初始化第0棵树

通过上面的“原理部分”的图,大家可能发现了,主席树在建树的时候,和普通线段树的建树操作不太一样。

对于普通线段树:根节点的编号是x,左子树的编号是x<<1,右子树是x<<1|1;

而对于主席树,由于多个节点重复使用,很显然不满足这个性质,所以,要用rt[i],ls[rt[i]],rs[rt[i]]分别存,树i的根节点编号,左子树根节点编号,右子树根节点编号。

我们用递归的方法,根节点编号为x,则左子树根节点为x+1,

再以左子树为根节点,递归其左子树,直到左子树为空,再去递归右子树,(相当于先序遍历,可结合上图理解一下主席树编号是怎么赋值的)

rt[i]:=树i的根节点编号是多少

ls[rt[i]]:=根节点编号为rt[i]的左子树的根节点编号

rs[rt[i]]:=根节点编号为rt[i]的右子树的根节点编号

//build(rt[0],1,nn);
void build(int &o,int l,int r){
    o=++tot;
    sum[o]=0;
    if(l==r)return ;
    int m=(l+r)>>1;
    build(ls[o],l,m);
    build(rs[o],m+1,r);
}

(3)从1到n建树操作

由“原理”部分可知,插入i节点的时候,建第i棵树,是在第i-1棵树的基础上建的,

先把新树,左子树的 ls 和 rs 都赋值成老树的 ls 和 rs,当然,其中一个必定会改变(每次更新只改变一条路径嘛)

更新的点p在其左子树中时,左节点肯定要变啦,右节点不变,直接用之前的节点就好啦!(rs[o]=rs[pre])

此时update(ls[o],l,m,ls[pre],p);

改变左子树的ls的值,和sum值

/*
for(int i=1;i<=n;i++){
    int x=lower_bound(a+1,a+1+nn,b[i])-a;
    update(rt[i],1,nn,rt[i-1],x);
}
*/
void update(int &o,int l,int r,int pre,int p){
    o=++tot;
    ls[o]=ls[pre];
    rs[o]=rs[pre];
    sum[o]=sum[pre]+1;//节点新插入一个元素,sum值比老sum值多1
    if(l==r)return ;
    int m=(l+r)>>1;
    if(p<=m)update(ls[o],l,m,ls[pre],p);
    else update(rs[o],m+1,r,rs[pre],p);
}

(4)查询

假设查询区间[l,r],则让第r棵树-第(l-1)棵树,在得到的这棵新树上查询就好啦

第r棵树根节点编号:rr=rt[r]

第r棵树左子树根节点的编号:ls[rr]

第r棵树左子树的sum值:sum[ls[rr]]

同理,第l-1棵树,左子树的sum值:sum[ls[lr]]

cnt=sum[ls[rr]]-sum[ls[lr]] := 第r棵树-第(l-1)棵树的左子树的sum值

若k<=cnt,查询左子树,

反之,查询右子树

//int ans=query(rt[l-1],rt[r],1,nn,k);
int query(int lr,int rr,int l,int r,int k){
    if(l==r)return l;
    int m=(l+r)>>1;
    int cnt=sum[ls[rr]]-sum[ls[lr]];
    if(k<=cnt)return query(ls[lr],ls[rr],l,m,k);
    else return query(rs[lr],rs[rr],m+1,r,k-cnt);
}

完结撒花✿✿ヽ(°▽°)ノ✿

看一道模板题:

洛谷P3834

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
#include<queue>
#include<stack>
#include<cmath>
#include<set>
#include<map>
using namespace std;
#define ll long long

typedef pair<int,int>P;
const int INF=0x3f3f3f3f;
const int N=200005;
int a[N],b[N],tot=0;
int rt[N],ls[N*20],rs[N*20],sum[N*20];

void build(int &o,int l,int r){
    o=++tot;
    sum[o]=0;
    if(l==r)return ;
    int m=(l+r)>>1;
    build(ls[o],l,m);
    build(rs[o],m+1,r);
}

void update(int &o,int l,int r,int pre,int p){
    o=++tot;
    ls[o]=ls[pre];
    rs[o]=rs[pre];
    sum[o]=sum[pre]+1;
    if(l==r)return ;
    int m=(l+r)>>1;
    if(p<=m)update(ls[o],l,m,ls[pre],p);
    else update(rs[o],m+1,r,rs[pre],p);
}

int query(int lr,int rr,int l,int r,int k){
    if(l==r)return l;
    int m=(l+r)>>1;
    int cnt=sum[ls[rr]]-sum[ls[lr]];
    if(k<=cnt)return query(ls[lr],ls[rr],l,m,k);
    else return query(rs[lr],rs[rr],m+1,r,k-cnt);
}

int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        b[i]=a[i];
    }
    sort(a+1,a+1+n);
    int nn=unique(a+1,a+1+n)-(a+1);
    int l,r,k;
    build(rt[0],1,nn);
    for(int i=1;i<=n;i++){
        int x=lower_bound(a+1,a+1+nn,b[i])-a;
        update(rt[i],1,nn,rt[i-1],x);
    }
    while(m--){
        scanf("%d%d%d",&l,&r,&k);
        int ans=query(rt[l-1],rt[r],1,nn,k);
        printf("%d\n",a[ans]);
    }
}

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值