主席树入门

  主席树又叫可持久化权值线段树,一开始使用来解决第k大的问题,因其发明者黄嘉泰名字的首字母和某人的一样,所以被叫做主席树。

 

  在了解主席树之前,我们先认识一下什么叫做权值线段树。

  给你n个数,问你这n个数中第k小的数是哪个。像这种题我们一般都是直接排序然后暴力找,但是我们今天用线段树来试试。

  例如a[12]={1,5,7,3,2,6,8,1,3,5,5,2},你要求出a数组中第7小的数的值。我们先建棵线段树用来存每个值出现的次数,按下标从小到大依次将数组中的元素放入插入线段树中。

  插入前三个数后,线段树就变成了这样

 

 

   继续插入元素,直至数组中的元素全部插入线段树中

  

 

  像这样按权值建的树就叫做权值线段树

 

  当全部插完后,就开始找第7小的数了。我们先从根开始,看看他左儿子的值是否大于k(这里k=7),假如左儿子的值大于等于k那么我们要找的第k小的数就在左边,否则就在右边。

  在这里左儿子也就是节点2的值为6,小于7,所以我们要找的数在右边,接下来我们要找以节点3为根的子树中最小的那个数。

  还是向上面那样一直递归,直到到了叶子节点,这时我们就找到了在数组中第7小的数了。

 

  接下来就是找区间内第k小的数了。对于任意[l,r]区间,我们先需要知道这个区间内有些什么数,然后再去找第k小。

  对于前一步,我们可以考虑建n棵权值线段树,第i棵权值线段树用来存[1,i]内各个数字的出现情况,这样我们就能直接利用前缀和的性质,让第r棵和第l-1棵权值线段树对应相减,这样我们就得到了[l,r]区间内数字的出现情况。

  然后我们就只需递归查找第k小就行了

 

  但是,要这样做的话我们就必须建n棵线段树,而每个线段树又要开四倍的空间,这样空间复杂度就太大了,我们必须要想办法去优化空间。

 

  我们仔细观察每次建树就会发现相邻两颗树只有logn个点不同,其余的都一样,就像第7棵权值线段树和第8棵权值线段树,他们只有划杠的那几个点不一样

 

 

   所以我们只需在上一颗树的基础上再开logn个点就行了,其他不变的结点直接继承上一颗树的对应结点,这样空间的开销就能接受了。

  

 

 

 

 

  

  主席树例题:https://www.luogu.com.cn/problem/P3834

  

  给你n个数,m个询问,每个问你[l,r]内的第k小的数是什么,n和m都是在1到2e5之间,数字大小在-1e9到1e9之间。

  直接上主席树,先建n棵树然后利用前缀和弄出区间内的数,最后在递归找第k小的那个数。

  

#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 200005
int n,m,cnt,a[maxn],b[maxn],root[maxn*40],L[maxn*40],R[maxn*40],sum[maxn*40];
//root存根的编号,L存左儿子的编号,R存右儿子的编号,sum存数字数量
int update(int pre,int l,int r,int pos)//pre为上个版本的编号,pos为位置 
{
    int rt=++cnt;//用rt存当前结点编号 
    L[rt]=L[pre];R[rt]=R[pre];
    //先将左儿子和右儿子全部等于上个版本对应的左右儿子,后面再看具体是要更新哪个儿子 
    sum[rt]=sum[pre]+1;//数量加加 
    if(l==r)return rt;
    int mid=l+r>>1;
    if(mid>=pos)L[rt]=update(L[pre],l,mid,pos);//更新左儿子 
    else R[rt]=update(R[pre],mid+1,r,pos);//更新右儿子 
    return rt;
}
int query(int x,int y,int l,int r,int k)//x和y分别代表第l-1棵树和第r棵树的结点编号,k为第k小 
{
    if(l==r)return l;//返回的是离散化后的数组中的位置 
    int mid=l+r>>1,tot=sum[L[y]]-sum[L[x]];
    //tot为当前结点,两棵树的左儿子所包含的数字数量之差 
    if(tot>=k)return query(L[x],L[y],l,mid,k);//如果tot>=k那就意味着第k小在左子树中 
    else return query(R[x],R[y],mid+1,r,k-tot);//否则在右子树中,接下来就是找右子树中第k-tot小的数 
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        b[i]=a[i];
        //一般数值范围都很大,直接按数值来建树会超空间,所以经常先进行离散化,然后再按离散化后的数组建树 
    }
    sort(b+1,b+1+n);//排序 
    int len=unique(b+1,b+1+n)-b-1;//去重 
    for(int i=1;i<=n;i++)
    {
        int pos=lower_bound(b+1,b+1+len,a[i])-b;//a[i]在b数组中的位置 
        root[i]=update(root[i-1],1,len,pos);//建第i棵线段树,第pos个数出现次数加加 
    }
    int l,r,k;
    while(m--)
    {
        cin>>l>>r>>k;
        cout<<b[query(root[l-1],root[r],1,len,k)]<<endl;
        /*查询第k小,利用第r棵树和第l-1棵树来找,注意query返回的是b数组中的位置
        从两棵树的根开始递归查,若左儿子满足则往左儿子递归否则往右儿子递归 
        */ 
    }
    return 0;
}

 

 

  对于主席树的空间大小,一般为O(nlogn)(不带修改),一般情况下开个40倍就够了。

posted on 2020-01-19 17:58  che027  阅读(380)  评论(0编辑  收藏  举报

导航