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

来源:互联网 发布:客管家软件好用吗 编辑:程序博客网 时间:2024/04/30 23:10

前置技能

线段树废话

主席树

介绍

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

主席树又称函数式线段树,顾名思义,也就是通过函数来实现的线段树,至于为什么叫主席树,那是因为是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 200005using 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 1000005using 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;}