主席树(可持久化线段树)

经典问题:求全局第K大

思路:可以在权值线段树上二分,当左儿子存储的个数大于k时在左儿子寻找,否则将k减去左儿子存储的个数在右儿子寻找

主席树经典题:求区间第k大

如果我们像全局第K大一样给每个区间建一个线段树是不可能的,考虑做一个前缀和,用第R个线段树减去第L-1个线段树就是[L,R]的线段树,再做全局第K大即可

但是仍然会MLE,事实上不需要每个数建一棵线段树,每次加入只需要插入的路径上更新即可,所以每次的空间复杂度是O(logn)

这样做实际上可以回到任何一个历史状态,这就是所谓的可持久化线段树——主席树

需要注意的是,因为要更新路径,我们按照遍历的顺序每到一个新节点编号加一(不同于传统线段树左儿子是P>>1右儿子是P>>1|1的编号方式)

 数组注释:lc[i]表示i的左儿子,rc[i]表示i的右儿子,rt[i]表示第i次更改时的根编号

build函数

void build(int &t,int l,int r)
{
    t=++cnt;//每访问一个新的节点就编号加一 
    if(l==r) return;
    int mid=(l+r)>>1;
    build(lc[t],l,mid);
    build(rc[t],mid+1,r);
}

比较简单,需要注意是按遍历顺序开点

modify函数(insert)

int modify(int o,int l,int r)//o表示上一个状态的编号 
{
    int h=++cnt;
    lc[h]=lc[o]; rc[h]=rc[o];/*左右儿子都复制一下*/ sums[h]=sums[o]+1;//当前节点处于插入路径上,需要更新 
    if(l==r) return h;
    int mid=(l+r)>>1;
    if(place<=mid) lc[h]=modify(lc[h],l,mid);//将lc[h]更新,成链状 
    else rc[h]=modify(rc[h],mid+1,r);
    return h;
}

比较重要的函数,可以结合图片理解

以下是插入数字4时的情况

query函数

//u是l-1时的线段树,v是r时的线段树
//query(rt[l-1],rt[r],1,num,k) 
int query(int u,int v,int l,int r,int k)
{
    int ans,mid=(l+r)>>1;
    int x=sums[lc[v]]-sums[lc[u]];//相当于[l,r]线段树lc[x]所存的个数 
    if(l==r) return l;//找到了第k小 
    if(x>=k) ans=query(lc[u],lc[v],l,mid,k);
    else ans=query(rc[u],rc[v],mid+1,r,k-x);
    return ans; //别忘了return 回去 
}

前缀和的思想,前缀减前缀,区间变全局

code

#include<cstdio>
#include<iostream>
#include<algorithm> 
#define N 500005
using namespace std;
int read()
{
    int x=0,f=1; char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
int a[N],index[N],rt[N],lc[N<<3],b[N];
int l,r,k,ans,num,place,n,m;
int cnt,sums[N<<3],rc[N<<3];
void build(int &t,int l,int r)
{
    t=++cnt;//每访问一个新的节点就编号加一 
    if(l==r) return;
    int mid=(l+r)>>1;
    build(lc[t],l,mid);
    build(rc[t],mid+1,r);
}
int modify(int o,int l,int r)//o表示上一个状态的编号 
{
    int h=++cnt;
    lc[h]=lc[o]; rc[h]=rc[o];/*左右儿子都复制一下*/ sums[h]=sums[o]+1;//当前节点处于插入路径上,需要更新 
    if(l==r) return h;
    int mid=(l+r)>>1;
    if(place<=mid) lc[h]=modify(lc[h],l,mid);//将lc[h]更新,成链状 
    else rc[h]=modify(rc[h],mid+1,r);
    return h;
}
//u是l-1时的线段树,v是r时的线段树
//query(rt[l-1],rt[r],1,num,k) 
int query(int u,int v,int l,int r,int k)
{
    int ans,mid=(l+r)>>1;
    int x=sums[lc[v]]-sums[lc[u]];//相当于[l,r]线段树lc[x]所存的个数 
    if(l==r) return l;//找到了第k小 
    if(x>=k) ans=query(lc[u],lc[v],l,mid,k);
    else ans=query(rc[u],rc[v],mid+1,r,k-x);
    return ans; //别忘了return 回去 
}
int main()
{
    n=read(); m=read();
    for(register int i=1;i<=n;++i){a[i]=read();b[i]=a[i];}
    sort(b+1,b+n+1);
    num=unique(b+1,b+n+1)-b-1;//离散化 
    build(rt[0],1,num);
    for(register int i=1;i<=n;i++)
    {
        place=lower_bound(b+1,b+num+1,a[i])-b;
        rt[i]=modify(rt[i-1],1,num); 
    }
    while(m--)
    {
        l=read(); r=read(); k=read();
        ans=query(rt[l-1],rt[r],1,num,k);
        printf("%d
",b[ans]); 
    }
    return 0;
} 
原文地址:https://www.cnblogs.com/Liuz8848/p/10454464.html