【主席树】可持久化线段树

主席树

可持久化线段树,又称为主席树,是线段树的进阶版。
本篇文章以可持久化权值线段树为例。如果不会权值线段树可以先学习一下。
【权值线段树】基础入门知识详解

是什么

它可以看作是多棵权值线段树,但它所占的空间很小!!!
具体不易解释,可以先往后面的内容浏览。

为什么要用它

对于一棵权值线段树,我们要往里面加入 n 个数。容易知道,每加入一个数就会更新一遍线段树。
当我们想要知道每次更新后的权值线段树的状态时,如果我们直接用 n 棵线段树,空间一般情况都会爆炸,于是这树我们要用到可持久化线段树——主席树
假设有这么一串数: 2 4 2 3
每次更新后的状态如果都用一棵普通权值线段树表示的话,是这样的:
这里写图片描述

这里写图片描述
图中红色的节点代表它的值相比上一次修改后的值发生了变化。
在这里,我们不难发现,每次修改只会有添加的值到根节点的一条链上的值发生了变化,而其它的节点和上次修改结束后的都是一样的。
既然如此,我们为什么要每次新建一棵权值线段树呢?每次新建一条链不就好了吗?

先看看它究竟长什么样

注意,前方超高能预警!!!
白色字体为一棵空的树,
红色为第一次添加的节点,
紫色为第二次添加的节点,
绿色为第三次添加的节点,
棕色为第四次添加的节点。
(为了画图好看,左右子树的位置可能相反,也就是可能左子树在右边,右子树在左边,分辨左右子树应看它们的区间所对应的值)
这里写图片描述
是不是特别震撼!!!现在的你是不是目瞪口呆!!!

怎么实现

首先我们要建立一棵没有值的权值线段树,如上图白色的地方。
修改时每到一个节点,判断要修改的值是在左子树还是右子树,新建将要修改的值所在的子节点,而另一边直接连向上一次修改的对应节点。
同时要记录每次修改的对应根节点编号。
注意:主席树的节点编号不一定满足 v 的左子树为 v 2 和右儿子为 v 2 + 1 ,所以必须用数组记录左右节点的编号。
举例:
如当前要修改 2 ,递归到区间 [ 1 , 4 ] 时, 2 在左儿子中,所以新建一个节点 [ 1 , 2 ] 为当前的左儿子,而右儿子就为上一次修改完的区间 [ 1 , 4 ] 的右儿子。

void make(int v,int l,int r)
{
    if(l==r) 
    {
        f[v].sum=0;
        if(v>num) num=v;//先记录空线段树所用的节点数。
        return;
    }
    else
    {
        int mid=(l+r)/2;
        f[v].l=v*2,f[v].r=v*2+1;
        make(v*2,l,mid);
        make(v*2+1,mid+1,r);
    }
}
void add(int v,int v1,int l,int r,int x)
{
    if(l==r)
    {
        f[v1].sum++;
        return;
    }
    else
    {
        int mid=(l+r)/2;
        if(x<=mid)//要修改的数在左子树中。
        {
            f[v1].l=++num;//该节点的左儿子为新建节点。
            f[v1].r=f[v].r;//右儿子为上一次修改后的右儿子。
            add(f[v].l,f[v1].l,l,mid,x);
        }
        else//要修改的数在右子树中。
        {
            f[v1].l=f[v].l;//左儿子为上一次修改后的左儿子。
            f[v1].r=++num;//该节点的右儿子为新建节点。
            add(f[v].r,f[v1].r,mid+1,r,x);
        }
        f[v1].sum=f[f[v1].l].sum+f[f[v1].r].sum;//当前的总和为左右节点的总和之和。
    }
}
root[0]=1;
make(1,1,n);//先建立一棵为空的权值线段树。
for(i=1;i<=n;i++)
{
    root[i]=++num;
    add(root[i-1],root[i],1,n,a[i]);//从上一个根和当前的根一起往下递归。
}

基本用处

求一个序列中,第 x 个数到第 y 个数中的第 k 小值。
和权值线段树求第 k 小值类似,每次从 x 1 y 的根节点开始往下递归。
每次的个数即为 y 树中对应个数减去 x 1 树中对应个数的值。

int find(int v,int v1,int l,int r,int k)
{
    if(l==r) return l;
    else 
    {
        int mid=(l+r)/2,s1=f[f[v1].l].sum-f[f[v].l].sum,s2=f[f[v1].r].sum-f[f[v].r].sum;//s1为第x~y个数中范围为[l,mid]的个数,s2为第x~y个数中范围为(mid,r]的个数。
        if(s1>=k) return find(f[v].l,f[v1].l,l,mid,k);
        else return find(f[v].r,f[v1].r,mid+1,r,k-s1);
    }
}
for(i=1;i<=q;i++)
{   
    scanf("%d%d%d",&x,&y,&k);
    printf("%d\n",find(root[x-1],root[y],1,n,k));
}

例题

JZOJ 1011. 【GDKOI2009模拟3】Zoo

猜你喜欢

转载自blog.csdn.net/qq_39565901/article/details/81782739