可持久化线段树——主席树

前言:
最近心(po)血(yu)来(ya)潮(li)学习了一下主席树。(再不学就落伍了)
主席树,即可持久化线段树,支持维护和查询区间的第\(k\)大(小)、区间不同种类个数等,基于线段树的思想之上

结构分析

主席树会维护\([1,n]\)中点的个数(可以理解为,一颗弹珠从树根放下,滑到叶节点时,走过的路径都\(+1\)
现在假设有一组数 \(\{a_1,a_2,...,a_7\}\),离散化后编号为 \(\{1,2,...,7\}\),现在以\(\{1,7,6,2,3,5,4\}\)的顺序进入主席树
下面来演示一下这个过程(PowerPoint冠名)








下面来张动图(PowerPoint冠名)

耐心等一会,总会等到GIF开头的(该动图时长32秒)

那么通过这种结构该如何寻找区间第\(k\)大(小)值呢?


算法实现

还是原来的数组 \(\{1,7,6,2,3,5,4\}\),现在来举个栗子:求\([2,5](\{7,6,2,3\})\)这一段的第\(2\)小值(\(3\)
首先要了解这样一个概念:求区间第\(k\)小值,相当于在区间内找一个值,使有\(k\)个数小于等于这个值(废话)
kgRAld.png
不(hen)难发现,在主席树里,左子树的值一定小于右子树的值。那么,只要判断左子树里的数的个数是否\(\ge2\),如果是,那么区间第\(2\)小值一定在左子树里,反之在右子树里
那么能否建\(N\)棵线段树呢?    :小朋友,我来了!
通过认真的观察,发现——


这两张图中,真正修改了值的节点并不多。这时,两棵线段树完全可以使用共用的节点,这样就可以大大的降低空间复杂度。那么,该如何处理\([l,r]\)中比当前值小的数的个数呢?
这里就要用到二分和前缀和的思想了。举个栗子,如果\([1,l-1]\)中有\(sum_1\)个数比当前值小,\([1,r]\)中有\(sum_2\)个数比当前值小,那么\([l,r]\)中就有 \(sum_2-sum_1\)个值比当前值小
回到刚才的问题:

数组 \(\{1,7,6,2,3,5,4\}\),求\([2,5](\{7,6,2,3\})\)这一段的第\(2\)小值(\(3\)

我们挑出第\((2-1)\)次插入时的树和第\(5\)次插入时的树,经过观察——

你发现了什么?
举个栗子,我们看上一棵树中的代表\([5,7]\)的节点和下一棵树的对应节点,可以发现:它们的差就是\(\{7,6,2,3\}\)中在\([5,7]\)上的数的个数。那么,上一棵树中的代表\([1,4]\)的节点和下一棵树的对应节点之差为\(2\),这说明第\(2\)小值必定在\([1,4]\)区间内,由此即可求出第\(2\)小值——\(3\)


代码实现

\(L,R\) 表示左右子树,\(sum\) 就是图中所维护的维护\([1,n]\)中点的个数,\(rank\) 为离散化数组,\(root\) 表示这么多线段树的根。答案递归求解即可。( \(fm(x)\) 表示初始化 \(x\) )

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define f(x,y) for(register int i=x;i<=y;i++)
#define G ch=getchar()
#define rd int
#define in(x) x=read();
#define node int
#define mid ((l+r)>>1)
#define fm(x) memset(x,0,sizeof(x))
#define maxN 200000
using namespace std;
inline rd read();
class president_tree
{
    public:
        node query(int x,int y,int w) {return rank[_query(root[x-1],root[y],1,_size,w)];}
        void build(int a[],int n)
        {
            tot=0; fm(rank); fm(root); fm(sum); fm(L); fm(R);
            for(register int i=1;i<=n;i++) rank[i]=a[i];
            sort(rank+1,rank+n+1);
            _size=unique(rank+1,rank+n+1)-rank-1;
            root[0]=_build(1,_size);
            for(register int i=1;i<=n;i++)
                root[i]=update(root[i-1],1,_size,lower_bound(rank+1,rank+_size+1,a[i])-rank);
        }
    private:
        int tot,_size;
        node rank[maxN+1],root[maxN+1],sum[(maxN<<5)+1],L[(maxN<<5)+1],R[(maxN<<5)+1];;
        node _build(int l,int r)
        {
            int k=++tot; sum[k]=0;
            if(l<r)
            {
                L[k]=_build(l,mid);
                R[k]=_build(mid+1,r);
            }
            return k;
        }
        node update(int pre,int l,int r,int w)
        {
            int k=++tot; L[k]=L[pre]; R[k]=R[pre]; sum[k]=sum[pre]+1;
            if(l<r)
                if(w<=mid) L[k]=update(L[pre],l,mid,w);
                else R[k]=update(R[pre],mid+1,r,w);
            return k;
        }
        node _query(int u,int v,int l,int r,int k)
        {
            if(l>=r) return l;
            int w=sum[L[v]]-sum[L[u]];
            if(w>=k) return _query(L[u],L[v],l,mid,k);
            else return _query(R[u],R[v],mid+1,r,k-w);
        }
};
int n,m,a[maxN+1],b[maxN+1],x,y,w;
president_tree tree;
int main()
{
    in(n); in(m); f(1,n) in(a[i]);
    tree.build(a,n);
    f(1,m)
    {
        in(x); in(y); in(w);
        printf("%d\n",tree.query(x,y,w));
    }
    return 0;
}
inline rd read()
{
    char ch=getchar();
    rd num=0,f=1;
    while((ch<'0' || ch>'9') && ch!='-') G;
    if(ch=='-') {f=-1; G;}
    while(ch>='0' && ch<='9') {num=num*10+ch-'0'; G;}
    return num*f;
}

写在最后

对主席树的认识与理解,到这篇博客的诞生,是起源于这两位巨佬的博客的——
最详细的讲解,让你一次学会主席树
【Notes】【主席树】hdu2665 Kth number
在此表示衷心的感谢!
(我才不会告诉你这篇博客写到一半未保存,网页就莫名其妙关闭了呢 QWQ)

posted @ 2019-02-19 20:35  常青藤的花语  阅读(139)  评论(0编辑  收藏  举报

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。