主席树/可持久化线段树简介(洛谷P3834/P3919)

前置技能

线段树废话

主席树

介绍

我太懒了所以直接引用一下

主席树又称函数式线段树,顾名思义,也就是通过函数来实现的线段树,至于为什么叫主席树,那是因为是fotile主席创建出来的这个数据结构

算法应用及实现

主席树最经典的应用就是在线求区间第k大。

运用前缀和的思想,我们把序列的每一个前缀都建一颗线段树。每一个节点存的是这个节点对应值出现的次数,所以查询 [l,r] 的时候只需要把 r 这棵树“减去”l1这棵树就行了。而我们只需知道他们的大小关系,因此预处理的时候先把它离散化一下。

但是如果把序列的每一个前缀都“真的”建树的话当然会MLE,而我们发现每次新加进去一个数最多只需要改变 log2 个节点的权值,其他的都不变。所以我们可以通过共用上一棵树的节点来减小空间开销。

差不多长这样(出处见右下角):

这里写图片描述

可以发现本来要新建7个点,通过共用之后只新建了3个点。这样一来,总空间就变成了 n(1+log2n) 了(1是因为要建一颗空树)。

查询的时候像平衡树一样,如果k小于右子树大小查询右子树第k大,否则查询左子树的第k-sum大。

代码(洛谷P3834):

#include<cctype>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 200005
using namespace std;
struct tree{
    //主席树的左右儿子编号并不是x*2和x*2+1
    //sum存子树大小(即这个子树的总次数)
    int ls,rs,sum;
}t[N*20];
int n,m,num,nd,rt[N*20],a[N],b[N];
inline char readc(){
    static char buf[100000],*l=buf,*r=buf;
    if (l==r) r=(l=buf)+fread(buf,1,100000,stdin);
    if (l==r) return EOF; return *l++;
}
inline int _read(){
    int x=0,f=1; char ch=readc();
    while (!isdigit(ch)) { if (ch=='-') f=-1; ch=readc(); }
    while (isdigit(ch)) x=x*10+ch-48,ch=readc();
    return x*f;
}
void ntlz(int &x,int l,int r){//建空树
    t[x=++nd].sum=0;
    if (l==r) return; int mid=l+r>>1;
    ntlz(t[x].ls,l,mid),ntlz(t[x].rs,mid+1,r);
}
void build(int &x,int l,int r,int fa,int p){//建树
    t[x=++nd].ls=t[fa].ls,t[x].rs=t[fa].rs;
    t[x].sum=t[fa].sum+1;
    if (l==r) return; int mid=l+r>>1;
    if (p<=mid) build(t[x].ls,l,mid,t[fa].ls,p);
    else build(t[x].rs,mid+1,r,t[fa].rs,p);
}
int srch(int p,int q,int l,int r,int k){//查询
    if (l==r) return l;
    int mid=l+r>>1,df=t[t[q].ls].sum-t[t[p].ls].sum;//直接相减
    if (k<=df) return srch(t[p].ls,t[q].ls,l,mid,k);
    else return srch(t[p].rs,t[q].rs,mid+1,r,k-df);
}
int main(){
    n=_read(),m=_read();
    for (int i=1;i<=n;i++) a[i]=b[i]=_read();
    sort(b+1,b+n+1),num=unique(b+1,b+n+1)-(b+1);//离散
    nd=0,ntlz(rt[0],1,num);
    for (int i=1;i<=n;i++)
        a[i]=lower_bound(b+1,b+num+1,a[i])-b;//a就是离散后的数组
    for (int i=1;i<=n;i++) build(rt[i],1,num,rt[i-1],a[i]);//建树
    while (m--){
        int l=_read(),r=_read(),k=_read();
        printf("%d\n",b[srch(rt[l-1],rt[r],1,num,k)]);
    }
    return 0;
}

可持久化线段树

其实就是主席树

介绍

看名称就知道是什么东西了。。。

算法应用与实现

看名称就知道是干嘛的了。。。

支持查询/修改某一历史版本的信息。

同主席树一样,对于每一次修改,不用重新建树,而是改变路径上的节点信息,共用其他节点。查询的话直接查就行了。

然后就差不多了。。。

洛谷P3919为例

#include<cctype>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 1000005
using namespace std;
struct tree{
    int ls,rs,x;
}t[N*40];
int n,m,nd,num,rt[N],a[N];
inline char readc(){
    static char buf[100000],*l=buf,*r=buf;
    if (l==r) r=(l=buf)+fread(buf,1,100000,stdin);
    if (l==r) return EOF; return *l++;
}
inline int _read(){
    int x=0,f=1; char ch=readc();
    while (!isdigit(ch)) { if (ch=='-') f=-1; ch=readc(); }
    while (isdigit(ch)) x=x*10+ch-48,ch=readc();
    return x*f;
}
void build(int &x,int l,int r){//建树
    int mid=l+r>>1; x=++nd;
    if (l==r) { t[x].x=a[l]; return; };
    build(t[x].ls,l,mid),build(t[x].rs,mid+1,r);
}
void nsrt(int &x,int l,int r,int p,int w,int fa){//修改
    t[x=++nd].ls=t[fa].ls,t[x].rs=t[fa].rs;//共用节点
    if (l==r) { t[x].x=w; return; } int mid=l+r>>1;
    if (p<=mid) nsrt(t[x].ls,l,mid,p,w,t[fa].ls);
    else nsrt(t[x].rs,mid+1,r,p,w,t[fa].rs);
}
int srch(int &x,int l,int r,int p,int fa){//查询
    t[x=++nd].ls=t[fa].ls,t[x].rs=t[fa].rs;//这道题要求两个操作都新增一个版本
    if (l==r) return t[x].x=t[fa].x; int mid=l+r>>1;
    if (p<=mid) return srch(t[x].ls,l,mid,p,t[fa].ls);
    else return srch(t[x].rs,mid+1,r,p,t[fa].rs);
}
int main(){
    n=_read(),m=_read();
    for (int i=1;i<=n;i++) a[i]=_read();
    build(rt[0],1,n);
    while (m--){
        int v=_read(),f=_read(),p=_read(),w;
        if (f==1) nsrt(rt[++num],1,n,p,w=_read(),rt[v]);
        else printf("%d\n",srch(rt[++num],1,n,p,rt[v]));
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值