zoukankan      html  css  js  c++  java
  • 树状结构之主席树

    主席树
    (看dalao们都写博客,我一个蒟蒻来凑个热闹)
    主席树看了1D,终于大致领悟了(^.^)
    首先我们来了解什么叫做主席树,下面是从其他大佬的博客中复制过来的了解一下就行

    所谓主席树呢,就是对原来的数列[1..n]的每一个前缀[1..i](1≤i≤n)建立一棵线段树(至于为什么要是前缀往下看...),线段树的每一个节点存某个前缀[1..i]中属于区间[L..R]的数一共有多少个(比如根节点是[1..n],一共i个数,sum[root] = i;根节点的左儿子是[1..(L+R)/2],若不大于(L+R)/2的数有x个,那么sum[root.left] = x)。若要查找[i..j]中第k大数时,设某结点x,那么x.sum[j] - x.sum[i - 1]就是[i..j]中在结点x内的数字总数。而对每一个前缀都建一棵树,会MLE,观察到每个[1..i]和[1..i-1]只有一条路是不一样的,那么其他的结点只要用回前一棵树的结点即可,时空复杂度为O(nlogn)。
    ---------------------
    是不是看的一脸懵逼,下面我就给大家翻译一下这段话。
    想学会主席树就要先学好线段树,因为主席树的本质就是多颗线段树串起来。线段树在此就不多说了。
    想要求区间的第k小,当然与当然与这个区间有多少数比他小有关,在这里我举一个例子来说明一下怎样建立这样的一颗线段树。比如一个数列的某个区间表示的序列是4,1,3,2,我们要求第2小,下面我画了一幅图来讲解,其中这颗线段树上的每个节点维护的是这个节点表示区间内的个数(比较丑,大家将就一下哈)。


    圈内的数字表示这个区间里面有多少个数,最后叶节点表示一个数字,对应上述序列中的一个数(因为每个数a[i]都在1e9内,而n只小于1e5,所以我们可以离散一下)。这样的话,想要找第k小的,先找左儿子节点,如果左儿子的叶子结点数多于k,那么第k大的数就在左儿子的叶子结点里面。否则就在右儿子里面。一直找到叶子结点,返回节点上的数就可以了。
    但多次询问区间的第k小,每次询问都建一棵树一定会TLE的,这样的话就使ta有了巨大的局限性,怎样优化一下呢?又废话了
    我们很容易就联想到了前缀和的概念,比如我们有一个问题。就是每次静态的求区间和,我们可以预处理所以的前缀和sum[i],我们每次求解区间l, r和时,我们可以直接得到答案为sum[r] - sum[l-1],这样就不需要对区间中的每个数进行相加来求解了。
    同样一个道理,我们也可以利用前缀和这个思想来解决建树这个问题,我们只需要建立n颗前缀线段树就行,第i树维护[1,i]序列,这样我们处理任意区间l, r时就可以通过处理区间[1,l - 1], [1,r],就行,然后两者的处理结果进行相加相减就行。为什么满足相加减的性质,我们简单分析一下就很容易得出。如果在区间[1,l-1]中有x个数小于一个数p,在[1,r]中有y个数小于p,那么在区间[l,r]中就有y - x 个数小于p,这样就很好理解为什么可以相加减了,而我们的主席树就是在已知y-x时用二分法来找p是多少。是不是理解一点了。
    若是每个点都建一颗数的话还是会MLE的,怎么办呢?
    例:有一个这样的数列
    5
    4 2 5 3 1
    以第一个数为根:


    以第二个数为根:


    我们会发现只有4个点各加了1,其他点都没变。所以我们就可以让两颗树共用这些没变的点:


    红色是新增的点,这样的话就只需多开4个点就可以了,是不是很省空间。后面的点依次累加上就好了。(图我就不画了哈,太大了(^-^))
    那么我们就是可以保证是没有问题,而且时间与空间复杂度都是稳定的树了
    上代码

     

     1 //主席树 
     2 #include<bits/stdc++.h>
     3 using namespace std;
     4 #define ll long long
     5 #define C continue
     6 #define B break
     7 #define N 200100
     8 inline int read()//快读不解释 
     9 {
    10     int f=1,x=0;
    11     char ch=getchar();
    12     while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
    13     while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
    14     return x*f;
    15 }
    16 int n,m,ql,qr,a[N],b[N],rs[N*25],ls[N*25],rt[N*25],kk,d[N*25],sz,t=0;//d表示这颗子数里插入多少数了。 
    17 void build(int &o, int l, int r)//先在0号点建一棵树 
    18 {
    19     o=++t;
    20     d[o]=0;
    21     if(l==r)    return ;
    22     int mid = (l + r) >>1;
    23     build(ls[o],l,mid);
    24     build(rs[o],mid+1,r);
    25 }
    26 void update(int &o,int l,int r,int la,int p)  //插入一个点 
    27 {
    28     o=++t;
    29     ls[o]=ls[la];
    30     rs[o]=rs[la];
    31     d[o]=d[la]+1;
    32     if(l==r)    return ;
    33     int mid=(l+r)>>1;
    34     if(p<=mid)update(ls[o],l,mid,ls[la],p);
    35     else    update(rs[o],mid+1,r,rs[la],p);
    36 }
    37 int query(int s,int e,int l,int r,int k)    //查询 
    38 {
    39     if(l==r)    return l;
    40     int an=d[ls[e]]-d[ls[s]];
    41     int mid=(l+r)>>1;
    42     if(k<=an)    return query(ls[s],ls[e],l,mid,k);
    43     else        return query(rs[s],rs[e],mid+1,r,k-an); 
    44 }
    45 int main()
    46 {
    47     n=read();m=read();t=0;
    48     for(int i=1;i<=n;i++)    a[i]=read(),b[i]=a[i];
    49     sort(b+1,b+n+1);
    50     sz=unique(b+1,b+n+1)-(b+1); //离散化不解释 
    51     build(rt[0],1,sz);
    52     for(int i=1;i<=n;i++)    a[i]=lower_bound(b+1,b+sz+1,a[i])-b;
    53     for(int i=1;i<=n;i++)    update(rt[i],1,sz,rt[i-1],a[i]);
    54     while(m--)
    55     {
    56         ql=read();qr=read();kk=read();
    57         int ans=query(rt[ql-1],rt[qr],1,sz,kk);
    58         printf("%d
    ",b[ans]);
    59     }
    60     return 0;
    61 } 
    View Code

    ------------------------------------------------------------------------------------------------------------------------------------------- 

    小结:(自己的一点总结,欢迎大家评论)

    做了几道主席树的题后,感觉主席树其实有点像高级的多个桶排,每颗线段树都是一个桶排数组,每个叶子结点就相当于桶排数组里的一个数。总共 n 个桶排数组,每个数组记录前i个数的桶排。对于桶排,想求区间[l,r]的小于k的数的累加和,就是把前 r 个数的桶排中小于 k 的数的累加和减去前 l-1 个数的桶排中小于 k 的数的累加和(小于k的什么什么什么(可以是数的个数,也可以是数的值)的累加和很好求,因为桶排数组是有序的)大家感性理解一下。把每个叶子结点想象成桶排中的一个数,而节点就是ta所对应的区间[L,R]的什么什么什么的累加和,就是L<=x[i]<=R的什么什么什么的累加和。

    至于为什么不直接用多个桶排写这类题,应该是会MLE吧,毕竟要开n个长度为n的数组,而n总是<=1e5的。为什么主席树不会爆空间,大家往上翻。

    自我感觉主席树最重要的操作就是求[l,r]内数值范围在[L,R]的什么什么什么的累加和。

     

  • 相关阅读:
    hiveserver2 with kerberos authentication
    python Basic usage
    python Quicksort demo
    Python HeapSort
    mrunit for wordcount demo
    CCDH证书
    Hadoop question list
    Hadoop Yarn core concepts
    Hadoop Resource
    Hadoop could not find or load main class
  • 原文地址:https://www.cnblogs.com/cylyz/p/10207007.html
Copyright © 2011-2022 走看看