***********************************************声明************************************************ 

原创作品,出自 “晓风残月xj” 博客,欢迎转载,转载时请务必注明出处(http://blog.csdn.net/xiaofengcanyuexj)。

由于各种原因,可能存在诸多不足,欢迎斧正!

*****************************************************************************************************     

      最近开始了自己高级数据结构之旅,在这次旅行中,我将持续把一些高级的数据结构从理论到编码都过一遍,同时通过博客形式分享出来,希望大家指出不足之处!

        本次所要学习的数据结构-主席树并不是什么新东西,不过是种线段树的变种,是一种可持久化的线段树;关于可持久化数据结构,一时找不到好的解释。下面是摘自大牛博客的解释:

 

可持久化数据结构

     可持久化数据结构(Persistent data structure)就是利用函数式编程的思想使其支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗。

         前面介绍过的伸展树就是一种典型的可持久化数据结构;本篇文章所要讲的主席树也是一种比较有特色的可持久化数据结构。关于可持久化数据结构,可以参考大牛博客:可持久化数据结构介绍 。

 

主席树

        主席树就是利用函数式编程的思想来使线段树支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗的增强版的线段树。

     很多问题如果用线段树处理的话需要采用离线思想,若用主席树则可直接在线处理。故很多时候离线线段树求解可以转化为在线主席树求解。注意,主席树本质就是线段树,变化就在其实现可持久化,后一刻可以参考前一刻的副本,二者共同部分很多。一颗线段树的节点维护的是当前节点对应区间的信息,倘若每次区间都不一样,就会给处理带来一些困难。有时可以直接细分区间然后合并,此种情况线段树可以直接敲定;但有时无法通过直接划分区间来求解,如频繁询问区间第k小元素,当然,此问题有比较特殊的数据结构-划分树。关于划分树,可以参考我的博客:划分树介绍。此时,除了划分树之外,主席树也是可以解决的。

     主席树的每个节点对应一颗线段树,此处有点抽象。在我们的印象中,每个线段树的节点维护的树左右子树下标以及当前节点对应区间的信息(信息视具体问题定)。对于一个待处理的序列a[1]、a[2]…a[n],有n个前缀。每个前缀可以看做一棵线段树,共有n课线段树;若不采用可持久化结构,带来的严重后果就是会MLE,即对内存来说很难承受。根据可持久化数据结构的定义,由于相邻线段树即前缀的公共部分很多,可以充分利用,达到优化目的,同时每棵线段树还是保留所有的叶节点只是较之前共用了很多共用节点。主席树很重要的操作就是如何寻找公用的节点信息,这些可能可能出现在根节点也可能出现在叶节点。

     下面摘自牛人理解所谓主席树呢,就是对原来的数列[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)。

      下面是我理解的一般形式上的主席树,如图:     

           

      主席树的很多很好的性质取决于操作的方便高效。下面依次介绍主席树的更新、查询等操作,给出效率分析。

1、建立

       首先建立一棵空的线段树,也是最原始的主席树,此时主席树只含一个空节点,然后依次对原序列按某种顺序更新,即将原序列加入到对应位置。此过程时间复杂度为O(M),空间复杂度为O(M*log(M))。如图:

          

2、更新

     更新时按照自己的离散值寻找对应位置,信息域data+1。我们知道,更新一个叶节点只会影响根节点到该叶节点的一条路径,故只需修改该路径上的信息域data。每个主席树的节点即每棵线段树的结构完全相同,只是对应信息域data不同(可以理解为线段树的结构完全一样,只是对应叶子节点取值不同,从而有些节点的信息域data不同,本质是节点节点不同),此时可以利用历史版本,即利用相邻的上一棵线段树的信息。相邻两颗线段树只有当前待处理的元素不同,其余位置完全一样。因此,如果待处理的元素进入线段树的左子树的话,右子树是完全一样的,可以共用,即直接让当前线段树节点的右子树指针指向相邻的上一棵线段树的右子树;若进入右子树,情况可以类比。此过程容易推出时间复杂度为O(log(M)),空间复杂度为O(log(M))。如图:

            

3.查询

     由于主席树每个节点是棵线段树,信息域、结构相同,可以相减。这是主席树查找的关键所在。例如查找第k小的元素,若左子树信息域data之差大于等于k,则直接到左子树查找;否则调整k值即减去左子树的信息域data之差,然后到相应的右子树查找。由于是线段树属于二叉树结构,故整个过程的时间复杂度为O(log(M)),M往往是原问题离散化后的数据数量级。对于任意主席树的节点即某棵线段树,其含义再次说明一下,存储的是原序列的某个前缀:a[1]、a[2]…a[k],其中k小于等于M,所以主席树节点i、j信息域data相减得到的即为原序列在区间[i,j]上的信息域data此过程时间复杂度为O(log(M)),。

 

4.实例代码

poj2104关于主席树用法ACM题目

poj2104具体题解

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;

const int MAXN=100000+100;
const int MAXM=MAXN*20;

int tot,n,m;
int da[MAXN],sDa[MAXN];
int leftChild[MAXM],rightChild[MAXM],wei[MAXM],chairTNode[MAXM];


/**********************************
*参数:待处理元素区间
*功能:建立一棵空线段树
*返回值:返回根节点下标
***********************************/
int Build(int left,int right)
{
    int id=tot++;
    wei[id]=0;
    if(left<right)
    {
        int mid=(left+right)>>1;
        leftChild[id]=Build(left,mid);
        rightChild[id]=Build(mid+1,right);
    }
    return id;
}


int Update(int root,int pos,int val)
{
    int l=1,r=m,mid,newRoot=tot++,retRoot=newRoot;
    wei[newRoot]=wei[root]+val;
    while(l<r)
    {
        mid=(l+r)>>1;
        if(pos<=mid)
        {
            //确定节点孩子节点
            leftChild[newRoot]=tot++;
            rightChild[newRoot]=rightChild[root];

            //确定待跟新节点以及历史版本
            newRoot=leftChild[newRoot];
            root=leftChild[root];

            r=mid;
        }
        else
        {
            rightChild[newRoot]=tot++;
            leftChild[newRoot]=leftChild[root];
            newRoot=rightChild[newRoot];
            root=rightChild[root];
            l=mid+1;
        }
        wei[newRoot]=wei[root]+val;
    }
    return retRoot;
}

int Query(int leftRoot,int rightRoot,int k)
{
    int  l=1,r=m,mid;
    while(l<r)
    {
        mid=(l+r)>>1;
        if(wei[leftChild[leftRoot]]-wei[leftChild[rightRoot]]>=k)//第k小值在左子树
        {
            //确定查找新区间
            leftRoot=leftChild[leftRoot];
            rightRoot=leftChild[rightRoot];

            r=mid;
        }
        else
        {
            k-=wei[leftChild[leftRoot]]-wei[leftChild[rightRoot]];
            leftRoot=rightChild[leftRoot];
            rightRoot=rightChild[rightRoot];
            l=mid+1;
        }
    }
    return l;
}

int main()
{
    int q,i;
    int ql,qr,k;
    while(scanf("%d%d",&n,&q)!=EOF)
    {
        m=0;
        tot=0;
        for(i=1; i<=n; i++)
        {
            scanf("%d",&da[i]);
            sDa[i]=da[i];
        }
        sort(sDa+1,sDa+n+1);
        m=unique(sDa+1,sDa+1+n)-sDa-1;
        chairTNode[n+1]=Build(1,m);
        //cout<<"**********"<<endl;

        for(i=n; i>=1; i--)
        {
            int pos=lower_bound(sDa+1,sDa+1+m,da[i])-sDa;
            chairTNode[i]=Update(chairTNode[i+1],pos,1);
        }
        while(q--)
        {
            scanf("%d%d%d",&ql,&qr,&k);
            printf("%d\n",sDa[Query(chairTNode[ql],chairTNode[qr+1],k)]);
        }
    }
    system("pause");
    return 0;
}



5.总结:

        由以上可知,主席树是一种特殊的线段树集合,属于可持久化线段树,具备线段树的几乎所有优势。此处可持久化我的理解就是可以充分利用历史版本,从而达到优化目的,故主席树更新、查找的时间复杂度为O(log(M)),且总的空间复杂度为O(M*log(M)),这些对于复杂问题都是可以接受的。关于主席树的代码,结合具体题目会在后面的有所展示,欢迎关注!

     由于写博客时间有限,难免有错误或不足的地方,欢迎斧正!