主席树(资料整理+深刻理解)(静态)

来源:互联网 发布:matlab centos ubuntu 编辑:程序博客网 时间:2024/06/05 19:03

  • 主席树
  • 一些链接
  • 算法理解
  • 代码理解
    • 离散
    • 插入

主席树

废话还是要说:

   主席树可以用来解决如下问题:“给出一列数,a1,a2…an,每次询问其中连续的一段区间ai到aj其中的第K大的数是多少?”   主席树的主体是线段树,准确的说,是很多棵线段树,存的是一段数字区间出现次数(所以要先离散化可能出现的数字)。举个例子,假设我每次都要求整个序列内的第 k 小,那么对整个序列构造一个线段树,然后在线段树上不断找第 k 小在当前数字区间的左半部分还是右半部分。这个操作和平衡树的 Rank 操作一样,只是这里将离散的数字搞成了连续的数字。

其实你们可以自己查


一些链接

1、算法过程图解:殇雪的blog
第三张图片很可以让我们很感性地了解主席树的优势和构造。
ta的博客是百度里第一个吧
2、博客最后一张图拯救了我对主席树的理解
就是这张图片,它很好地展示了主席树中应对”n颗树“的空间优化。
可以看作建树后(黑线组成的树)第一次插入新值(也就是红线部分)组成的第二棵树(红线部分以及红线下的黑线部分 共同组成的树)


图比较简单好懂。

算法理解

网上的代码要么就是数组型,要么就是传很多参数的struct型。
搞得我每学一个算法就需要重新规划代码思路,重新构建代码风格。
所以我尝试用我的理解和习惯,尽量把不必要的参数存在结构体里了。

建树:就是线段树。但是值得注意的是:是否需要建立初始树,也就是root[0]领导的树。
如果不建,那么就需要很耐心地在各个子函数里传区间参数[l,r]。习惯用数组的朋友。
如果建,就可以不需要,那就得把l,r存在struct里。
(不对请指正,谢谢。)

更新:主席树根据n个数据,会建立n个线段树,即每插入(和更新有点不一样,更新是同一位置赋值,插入是在初始化的位置上赋值,前者是动态主席树,后者是静态。)一个数据,就建一棵树(i),显然除了一条路径不同之外,树的其他部分可以完全不必要再开空间,直接继承于第i-1颗树,继承是直接把第i棵树的不变区间连到上一棵树对应区间。他们之间只有log(n)个节点是不同的,每次新增的空间只需要log(n),每次更新也是log(n)的时间。

意义:我们要修改一个叶子结点的值,并且不能影响旧版本的结构。
在从根结点递归向下寻找目标结点时,将路径上经过的结点都复制一份。
找到目标结点后,我们新建一个新的叶子结点,使它的值为修改后的版本,并将它的地址返回。
对于一个非叶子结点,它至多只有一个子结点会被修改,那么我们对将要被修改的子结点调用修改函数,那么就得到了它修改后的儿子。
在每一步都向上返回当前结点的地址,使父结点能够接收到修改后的子结点。
在这个过程中,只有对新建的结点的操作,没有对旧版本的数据进行修改。

注意:
1、每一棵树都有各自独立的根,每个根与更新次数一一对应,存在一个数组里(比如叫它:root[i])
2、第i次更新,对应树存的数据管理范围是[1,i],有类似前缀的意味。
于是这样的结构具有了可加减性,所以我们不但可以查询[1,x],还可以查询[x,y],那么[x,y]=[1,y]-[1,x-1];

查询:这里比普通线段树多传一个关键的数据,就是”哪一次询问“,同样位置可以维护不同的更新时间的数据。(“可持续化”就是这个意思。)

于是我们有了如下经典问题:
(摘自cyendra的博客)

区间第K小值问题
有n个数,多次询问一个区间[L,R]中第k小的值是多少。

查询[1,n]中的第K小值
我们先对数据进行离散化(离散后的数据放在了rank[ ]里),然后按值域建立线段树,线段树中维护某个值域中的元素个数。
在线段树的每个结点上用cnt记录这一个值域中的元素个数。
那么要寻找第K小值,从根结点开始处理,若左儿子中表示的元素个数大于等于K,那么我们递归的处理左儿子,寻找左儿子中第K小的数;
若左儿子中的元素个数小于K,那么第K小的数在右儿子中,我们寻找右儿子中第K-(左儿子中的元素数)小的数。

查询区间[L,R]中的第K小值
我们按照从1到n的顺序依次将数据插入可持久化的线段树中,将会得到n+1个版本的线段树(包括初始化的版本),将其编号为0~n。(0就是初始线段树。)

考虑第i个版本的线段树的结点P,P中储存的值表示[1,i]这个区间中,P结点的值域中所含的元素个数;
假设我们知道了[1,R]区间中P结点的值域中所含的元素个数,也知道[1,L-1]区间中P结点的值域中所包含的元素个数,显然用第一个个数减去第二个个数,就可以得到[L,R]区间中的元素个数。
因此我们对于一个查询[L,R],同步考虑两个根root[L-1]与root[R],用它们同一个位置的结点的差值就表示了区间[L,R]中的元素个数,利用这个性质,从两个根节点,向左右儿子中递归的查找第K小数即可。

代码理解

模板题。

离散

//全局部分struct nodd {    int v, id ;    friend bool operator < ( nodd a, nodd b )     {        return a.v == b.v ? a.id < b.id : a.v < b.v ;    }} a[maxn] ;//离散部分for(i=1;i<=n;i++)     {        Read(a[i].v) ;        a[i].id =i;    }    sort(a+1,a+n+1) ;    for(i=1;i<=n;i++)        Rank[a[i].id]=i ;//其实比较好理解,不理解手推就可以。

插入

阅码注意:
1、哪里有取地址符?
2、变量是全局变量,还是局部变量,还是结构体中的变量?
更新函数\插入函数
3、其实这里的代码很重要,其他类型的主席树问题这里是基本不变的。思想很重要

//主函数部分for (i=1;i<=n;i++) {    rt[i]=rt[i-1] ;//1-这一句是灵魂吧。要说的太多了,我写在下面。    insert(Rank[i],rt[i],1,n) ;//2-把离散后的第i个数,插入到root[i]领导的主席树中,区间长度l,r。}//子函数部分void insert ( int K, int& x, int l, int r ) //3-额,这个k是数据key,不是第k大的k。x前面有一个取地址,哈。{    tree[++cnt]=tree[x] ;//4-    x=cnt;//5-    tree[x].v++;//6-增加数量    int mid =(l+r)>>1 ;//7-没啥说的,占地方    if(l==r)return ;    if(K<=mid) insert(K,x[tree].l,l,mid) ;//8-为什么不是teer<<1?    else insert(K,x[tree].r,mid+1,r) ;//9-为什么不是teer<<1|1?}

1-从一开始理解,我们有一颗空树。如果我们不这样写,我们直接传root[1],那当我们递归到我们不需要改的区间,我们根本无法找到初始树的位置,所以暂且把初始树的root完全复制过来.
这样,因为每一颗树的根节点储存了它儿子的标号,所以这一步我们相当于第1棵树完全等价于第0棵树,只多开了一个root,其他的已经!!完全检索!!到上一棵树里。
然后在新的树里把不一样的树枝修改成新的值,并且新开空间就可以了。所以之后递归就是if,else if,而不是if,if.只选择一条路。那么以此类推,第i棵树继承第i-1棵树就行了。

3-取地址,为啥呢?我们看主函数里是怎么调用的:

 insert(Rank[i],rt[i],1,n) ;

从一开始理解:也就是说我们要在递归的时候修改root[i]里存的值,以前存的是上一棵树的编号,多没面子啊,我们要给每一个根一个实际的地位,(为啥我就不废话了)
所以一进去我们就根据调用次数cnt来创造一个新的节点“tree[++cnt]”(4-),然后 改变x(x=cnt)(5-),也就是把rt[i](为什么这里是rt明白了吧,他就是个索引,真的root被他索引,上面提到的root都是rt)指向了root[++cnt]这个真的根节点。

6-因为刚才提到了可加减性,数 第i次修改时 这个节点管辖范围内的有小数据有几个。

8-9-从这个函数一开始我们就可以知道:
节点编号的机制不是 *2 和*2+1
而是根据调用次数由cnt管控。所以要存在每一个节点里。

int query ( int rt1, int rt2, int l, int r, int K ) {    if (l==r) return l;    int mid=(l+r)>>1;    int num=tree[rt2].l[tree].v - tree[rt1].l[tree].v ;//可加减性,为啥这样做,因为几点存的是前缀和,我们要特定区间就把前面的减去咯。你也可以这样认为:假设查第999大,当前节点总共管3个数,你就需要把前面没用的减去咯。    if ( K <= num )         return query(rt1[tree].l,rt2[tree].l,l,mid,K) ;    else         return query(rt1[tree].r,rt2[tree].r,mid+1,r,K-num ) ;}

附上完整代码:
以后改一改吧,用的WT_cnyali的代码。比较习惯这样代码风格的。

#include <bits/stdc++.h>using namespace std ;bool Read ( int &x ) {     char c = getchar() ; x = 0 ; bool f = 0 ;     while ( !isdigit(c) )     {         if ( c == '-' ) f = 1 ;         if ( c == EOF ) return false ; c = getchar() ;     }     while ( isdigit(c) )     { x = 10 * x + c - '0' ; c = getchar() ; }     if (f) x = -x ;return true ; }void Print ( int x ) {     int len=0,a[50] ;     if ( x == 0 ) { putchar('0') ; return ; }     if ( x < 0 ) { putchar('-') ; x = -x ; }     while (x) { a[++len] = x%10 ; x /= 10 ; }     while (len) putchar(a[len--]+'0') ;}const int maxn=200010 ;int n,m,rt[maxn],Rank[maxn],cnt ;struct node {    int l, r, v ;} tree[maxn<<5] ;struct nodd {    int v, id ;    friend bool operator < ( nodd a, nodd b )     {        return a.v == b.v ? a.id < b.id : a.v < b.v ;    }} a[maxn] ;void insert ( int K, int& x, int l, int r ) {    tree[++cnt]=tree[x] ;    x=cnt;    tree[x].v++;    int mid =(l+r)>>1 ;    if(l==r)return ;    if(K<=mid) insert(K,x[tree].l,l,mid) ;    else insert(K,x[tree].r,mid+1,r) ;}int query ( int rt1, int rt2, int l, int r, int K ) {    if ( l == r ) return l ;    int mid = l+r >> 1 ;    int num = tree[rt2].l[tree].v - tree[rt1].l[tree].v ;    if ( K <= num ) return query ( rt1[tree].l, rt2[tree].l, l, mid, K ) ;    else return query ( rt1[tree].r, rt2[tree].r, mid+1, r, K-num ) ;}int main() {    int i,j,k,x,y;    Read(n) ; Read(m) ;    for (i=1;i<=n;i++)     {        Read(a[i].v) ;        a[i].id=i ;    }    sort (a+1,a+n+1) ;    for (i=1;i<=n;i++)         Rank[a[i].id]=i ;    for (i=1;i<=n;i++)     {        rt[i]=rt[i-1] ;        insert(Rank[i],rt[i],1,n) ;    }    while(m--)     {        Read(x);Read(y);Read(k) ;        Print(a[query(rt[x-1],rt[y],1,n,k)].v ) ;        putchar('\n') ;    }    return 0 ;}
原创粉丝点击