蒟蒻林荫的小复习——主席树

Posted xlinyin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了蒟蒻林荫的小复习——主席树相关的知识,希望对你有一定的参考价值。

主席树也就是指可持久化线段树,大致思想也就是每次利用之前的重复信息,只为被更新的一部分开辟新点。而且所谓可持久化线段树实际上是指可持久化权值线段树,线段树中每个端点存的是这个端点所代表的树的出现次数。

而在主席树的维护当中对于每个历史版本如果都开一颗新树,那么M将是最终的结局。正确解法则是为每一个改变的点分配编号而未改变的点直接链接到新树上。

下面就来从主席树的两个经典问题来考虑

1:区间第K小问题 传送门

首先分析问题,给出一个序列,每次给定一个封闭区间,求这个封闭区间以内的最小值。

上面我们说过,主席树实际上是一颗权值线段树,点里面存的是点所代表这个数出现了多少次。先看数据范围,离散化是跑不了的。反正n<=2*10^5

那么就把原数列离散化到1---2*10^5的区间之内。开n棵树,第i颗代表压入前i个数之后各个数字的出现情况。

声明一个3元组,分别代表左儿子编号,右儿子编号,当前节点子树内所有元素共计出现次数(如果是叶子节点就代表叶子节点代表的这个元素出现次数)

所以说动态建树不用解释了吧

int build(int l,int r)

    int rt=++tot;
    if(l<r)
    
        L[rt]=build(l,mid);//每个节点构造线段树中左子树位置 
        R[rt]=build(mid+1,r);//每个节点构造线段树中右子树位置 
    
    return rt;

build(1,m)m为不同的元素个数。

下面就到了插入了。很明显,插入一定是在上个版本的基础上进行的。那么就先让新版本的左右子树都等于旧版本的,但是新版本根节点元素个数=老版本的+1。然后再判断新插入的数的左右区间隶属,因为已经进行了离散化,所以新插入的数会按照大小分给左右区间。叶子节点所表示的区间即为x的值

int update(int root,int l,int r,int x)
//x即为离散化后的带插入数

//代码前面有#define mid (l+r)/2
    int rt=++tot;
    L[rt]=L[root];
    R[rt]=R[root];
    sum[rt]=sum[root]+1;
    if(l<r)
    
        if(x<=mid)
        
            L[rt]=update(L[root],l,mid,x);
            
        
        else
        
            R[rt]=update(R[root],mid+1,r,x);
        
    
    return rt;

下面就是查询了,选择L-1版和R版,每一次求得sum[L[now]]-sum[L[last]]如果该结果>=k,则证明第K小的值在当前子树的左子树中,否则在右子树中。

int query(int u,int v,int l,int r,int k)

    if(l==r)
    
        return l;
    
    int x=sum[L[v]]-sum[L[u]];
    if(x>=k)
    
        return query(L[u],L[v],l,mid,k);
    
    else
    
        return query(R[u],R[v],mid+1,r,k-x);
    

然后就这样的写完了,还有一点就是离散化部分需要注意一下。

技术图片
// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long int 
#define mid (l+r)/2
const int N=200001;
bool function(int a,int b)

    return a<b;

const int LOG=20;
int n,m,q,tot=0;
int a[N],b[N];
int T[N],sum[N*LOG],L[N*LOG],R[N*LOG];
int build(int l,int r)

    int rt=++tot;
    if(l<r)
    
        L[rt]=build(l,mid);//每个节点构造线段树中左子树位置 
        R[rt]=build(mid+1,r);//每个节点构造线段树中右子树位置 
    
    return rt;

int update(int root,int l,int r,int x)

    int rt=++tot;
    L[rt]=L[root];
    R[rt]=R[root];
    sum[rt]=sum[root]+1;
    if(l<r)
    
        if(x<=mid)
        
            L[rt]=update(L[root],l,mid,x);
            
        
        else
        
            R[rt]=update(R[root],mid+1,r,x);
        
    
    return rt;

int query(int u,int v,int l,int r,int k)

    if(l==r)
    
        return l;
    
    int x=sum[L[v]]-sum[L[u]];
    if(x>=k)
    
        return query(L[u],L[v],l,mid,k);
    
    else
    
        return query(R[u],R[v],mid+1,r,k-x);
    

int main()

    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)
    
        scanf("%d",&a[i]);
        b[i]=a[i];
    
    sort(b+1,b+1+n,function);
    m=unique(b+1,b+1+n)-b-1;
    T[0]=build(1,m);
    for(int i=1;i<=n;i++)
    
        a[i]=lower_bound(b+1,b+1+m,a[i])-b;
        T[i]=update(T[i-1],1,m,a[i]);
    
    while(q--)
    
            int x, y, z; 
            scanf("%d%d%d", &x, &y, &z);
            int p = query(T[x-1], T[y], 1, m, z);
            printf("%d\n", b[p]);    
    
    return 0;
AC

2:长得很像普通线段树的可持久化线段树

这种题就没有必要写权值线段树了,毕竟权值线段树写起来很难受。

然而说实话这道题挺水,真的就是线段树的可持久化,直接上代码吧

#include<iostream>
#include<cstdio>
using namespace std;
struct POINT

    int ls,rs,maxx;
;
POINT t[100001*16];
int n,m,tot=1;
int a[10001];
int Root[100001];
void pushup(int x)

    t[x].maxx=max(t[t[x].ls].maxx,t[t[x].rs].maxx);

void build(int rt,int l,int r)

    if(l==r)
    
        t[rt].maxx=a[l];
        return;
    
    t[rt].ls=++tot;
    t[rt].rs=++tot;
    int mid=(l+r)>>1;
    build(t[rt].ls,l,mid);
    build(t[rt].rs,mid+1,r);
    pushup(rt);

void CHANGE(int rt1,int rt2,int l,int r,int pos,int ke)

    if(l==r)
    
        t[rt1].maxx=ke;
        return ;
    
    int mid=(l+r)>>1;
    if(pos<=mid)
    
        t[rt1].rs=t[rt2].rs;
        t[rt1].ls=++tot;
        CHANGE(t[rt1].ls,t[rt2].ls,l,mid,pos,ke);
    
    else
    
        t[rt1].ls=t[rt2].ls;
        t[rt1].rs=++tot;
        CHANGE(t[rt1].rs,t[rt2].rs,mid+1,r,pos,ke);
    
    pushup(rt1);

int QUERY(int rt,int l,int r,int nl,int nr)

    if(nl<=l&&r<=nr)
    
        return t[rt].maxx; 
    
    if(l==r)
    
        return t[rt].maxx;
    
    int sed=-998244353;
    int mid=(l+r)>>1;
    if(nl<=mid)
    
        //cout<<t[rt].ls<<‘ ‘<<l<<‘ ‘<<mid<<endl;
        sed=max(sed,QUERY(t[rt].ls,l,mid,nl,nr));
    
    if(nr>mid)
    
        //cout<<t[rt].rs<<‘ ‘<<mid+1<<‘ ‘<<r<<endl;
        sed=max(sed,QUERY(t[rt].rs,mid+1,r,nl,nr));
    
    return sed;

void PDDFS(int x)

    if(x==0)
        return;
    cout<<x<<endl;
    PDDFS(t[x].ls);
    PDDFS(t[x].rs);

int main()

    freopen("longterm_segtree.in","r",stdin);
    freopen("longterm_segtree.out","w",stdout);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    
        scanf("%d",&a[i]);
    
    build(1,1,n);
    int a1,a2,a3,a4;
    Root[1]=1;
    int cnt=1;
    while(m--)
    
        scanf("%d%d%d%d",&a1,&a2,&a3,&a4);
        if(a1==0)
            //PDDFS(Root[a2]);
            cout<<QUERY(Root[a2],1,n,a3,a4)<<endl;
            
        
        else
        
            Root[++cnt]=++tot;
            CHANGE(Root[cnt],Root[a2],1,n,a3,a4);
        
    
    return 0;

完结撒花!

以上是关于蒟蒻林荫的小复习——主席树的主要内容,如果未能解决你的问题,请参考以下文章

蒟蒻林荫小复习——带权并查集

蒟蒻林荫小复习——KMP算法

蒟蒻林荫小复习——K短路的A*解法

poj2104(主席树讲解)

主席树 或者 离散化+分块 BZOJ 4636

主席树的学习