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

前言

很久以前就听说过主席树这个名字,当时听到的时候还只能一脸懵逼,不知道是什么意思。而现在学习了“线段树”之后终于可以对这种神奇的“线段树”进行学习。虽然说我并不知道“主席树”与可持久化权值线段树到底有着什么样的联系,甚至不知道什么是“可持久化”,但是这两个陌生的名词在机房学长们的口中就好像是同一个意思,因此我把这篇文章的标题写成了:“主席树——可持久化权值线段树”。以下是机房学长的两篇博客。

goseqh ‘s 浅谈主席树

高杨大神 ‘s 【模板-7】主席树 Orz

主席树可以理解成“可持久化权值线段树”,由此观之,“线段树”的知识对于“主席树”的学习来说是尤为重要的,没有学过“线段树”的同学建议看一下有关的博客:

我的博客:我与线段树的故事

(有“主席树”为什么没有“总理树”,╮(╯▽╰)╭哎~)


1.线段树的“随波逐流”

线段树是我学习的第一种“牛13”的数据结构,它可以支持强大的区间修改和区间查询等操作。后来我遇到了这样的一道题:给定一个序列,询问这个区间的“区间第k大”。本以为支持强大区间操作的“线段树”能够胜任,结果发现“线段树”捉襟见肘了。我怎么也想不到一种方发用线段树维护出“区间第k大的属性”,因为这个区间属性太特殊了,这怎么办呢?

在学习线段树的时候,我们可以用O(lgn)的时间用二分法查找一个结点所处的位置,这是线段树中结点与原序列的关系。而现在我们要找到的是“第k大”,假如给原序列排序,排序之后的“第k个”就是我想要找的答案。这次,我们要找到的就不是“结点与原序列的关系了”而是“结点与‘数集’之间的关系”。也就是说我们可以为”数集”建立一棵线段树。

构造线段树

在这棵线段树里,每一个结点表示一个“数值区间”,结点的属性表示原序列有多少个数值在这个“数值区间”当中,它的建立和其他的线段树也是没什么两样的。如果我统计出了每一个“数值区间”所包含的元素个数就可以很方便的确定“第k大”的数了。假如原序列的数值范围为[1,8],我要找到它的“第k大”。我就要去判断“数值范围”在[1,4]中的数个数是否大于k。如果是,说明第k大的数在[1,4],否则在[1,8],这样就可以递归求出这个序列的“k大值”。

查询第k大


2.主席树

你会发现,如果只用刚才的那个“数值区间”的线段树是不能求出“区间第k大”的,因为它只能求出一个序列整体的“第k大”,而不能拓展到任意区间。然而有这样的一个性质:如果我们用“M(i,j,x,y)”表示原序列中 从第i个数开始到第j个数结束 的区间 满足“数值区间”[x,y]的元素的个数,那么就有:

M ( i , j , x , y ) = M ( 1 , j , x , y ) - M ( 1 , i - 1 , x , y )

(看明白之后往下看。)

这就说明,我不用给每个区间建立线段树,只要给原序列的每个前缀建立一个线段树就能求得所有区间与“数集”的关系。也就是说我们可以建立“一群线段树”来维护这个性质:

一群线段树

这看起来是不是像一个立体的结构。

不过要注意的是,这里的每一棵线段树都有MlgM个结点,一共有N棵线段树,所以说空间复杂度一定会炸,而且构建这么多完整的线段树时间也一定会炸。但是你会发现,其实这些线段树都是非常相似的,因为两个长度只差1的前缀之间只有一个不同的数,因此它与前一个线段树只有一条支路是不同呢?那么为什么不让这些线段树共用一部分树枝呢?这听起来很疯狂,不过却很可行。

构造主席树

我们可以先为“空前缀”建立一棵“空线段树”,里面的权值全是零。然后依次为每一个前缀建立线段树,与前一棵树相同的区间直接接到前一棵树的对应节点上,不同的部分再新申请结点。比如:当我构造当前前缀的线段树的[1,4]数值区间时,如果当前前缀比上一个前缀新增的那个数为3或4,就把当前结点的左子接到上一个线段树的[1,2]区间上;如果新增的那个数为1或2,就把当前结点的右子接到上一个节点的[3,4]区间上。然后再用同样的方法递归处理新添加的结点。这样除了空树以外,每一个前缀只需要lgM个结点用于建树,空间复杂度为O((N+M)lgM),比较可以接受。

我们可以储存一个treeRoot数组,用treeRoot[i]表示第i个前缀所对应的线段树的根节点,以便于以treeRoot[i+1]为根的线段树的构建。


3.主席树的构造

给出我自己的模板,我的代码可能比较有个性:

#include<iostream>
#include<cstdlib>
using namespace std;

struct NODE//定义树节点
{
    int l,r;
    int lch,rch;
    int num_count;
    void clr(int L,int R,int LCH,int RCH, int NUM_COUNT)
    {
        l=L;r=R;lch=LCH;rch=RCH;num_count=NUM_COUNT; 
    }
}ns[1048576];
int newnode=0;//当前结点个数(用于申请结点)
int array[1048576];//原序列
int root_array[1048576];//也就是上文说的treeRoot

void build_empty_tree(int root,int l,int r)//建立一棵空树,"数值范围"为[l,r]
{
    if(l==r)//如果确定到唯一数值
    {
        ns[root].clr(l,r,-1,-1,0);
        return;
    }
    int nl=newnode++;
    int nr=newnode++;//申请左子和右子
    int mid=(l+r)/2;
    build_empty_tree(nl,l,mid);
    build_empty_tree(nr,mid+1,r);//递归定义左子和右子
    ns[root].clr(l,r,nl,nr,0);
}

void build_tree(int last_root,int root_now,int num_now)//建立一个前缀的线段树(局部)
{
    ns[root_now]=ns[last_root];//先把原先根节点的内容复制给当前根节点
    if(ns[root_now].l==ns[root_now].r)//如果确定到唯一数值
    {
        ns[root_now].num_count++;//这个数值的统计数加一
        return;
    }
    int mid=(ns[root_now].l+ns[root_now].r)/2;
    if(num_now<=mid)//如果新添的数值在左子树
    {
        ns[root_now].lch=newnode++;//新申请一个左子结点
        build_tree(ns[last_root].lch,ns[root_now].lch,num_now);//递归定义左子
    }else
    if(mid<num_now)//如果在右子树
    {
        ns[root_now].rch=newnode++;//申请一个右子结点
        build_tree(ns[last_root].rch,ns[root_now].rch,num_now);//递归定义右子
    }
    ns[root_now].num_count++;
}

int query(int root_i,int root_j,int k)//询问两颗线段树之间所夹区间"k大值"
{
    if(ns[root_j].l==ns[root_j].r)//确定到唯一结点
        return ns[root_j].l;
    int left_count=(ns[ns[root_j].lch].num_count)-(ns[ns[root_i].lch].num_count);//左子中数字个数
    if(k<=left_count)//k大值在左子
    {
        int a=query(ns[root_i].lch,ns[root_j].lch,k);
        return a;
    }else{//k大值在右子
        int a=query(ns[root_i].rch,ns[root_j].rch,k-left_count);
        return a;
    }
}

int main()
{
    int n;
    cout<<"n=";//输入序列长度
    cin>>n;
    int x,y;
    cout<<"x,y=";//输入数值范围
    cin>>x>>y;
    cout<<"array[1..n]=";//输入序列
    for(int i=1;i<=n;i++)
        cin>>array[i];
    root_array[0]=newnode++;//申请空树根结点
    build_empty_tree(root_array[0],x,y);//建空树
    for(int i=1;i<=n;i++)
    {
        int root_now=newnode++;//申请一个新结点
        root_array[i]=root_now;
        build_tree(root_array[i-1],root_now,array[i]);//为当前前缀建立线段树
    }
    int m;
    cout<<"m=";//输入询问次数
    cin>>m;
    for(int i=1;i<=m;i++)
    {
        cout<<"L,R,k=";
        int L,R,k;//输入询问,原序列中的[L,R]区间的k大值
        cin>>L>>R>>k;
        cout<<"query="<<query(root_array[L-1],root_array[R],k)<<endl;//计算询问
    }
    system("pause");
    return 0;
} 
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值