可持久化线段树(主席树)学习笔记

版权声明: https://blog.csdn.net/qq_16267919/article/details/79950173
0x00写在前面

在接触可持久化线段树前一直以为这是一个高大上的东西,非常复杂.但是在学习之后才发现它远比想象中容易,甚至比线段树还简单,所以学的时候不要有心理压力.

0x01从一道题开始

洛谷P3834
题意:给你一个序列,多次询问区间第k大的数是多少(序列长度与询问个数为1e5级别)
可以考虑这样一种算法:对于每个前缀[1,i]维护一颗权值线段树,每次询问[l,r]时只需用前缀[1,r]减去前缀[1,l1]的线段树的对应节点就可以得到[l,r]的权值线段树,就可以轻松得到区间第k小.
但是建这么多线段树显然是不可能的,通过观察我们发现相邻两颗线段树都只相差一次修改,即[1,i]是在[1,i1]的基础上插入i处的数得到的.因此我们考虑利用这一性质,这就引出的本文的主题:可持久化线段树.

0x02可持久化线段树简介

一次修改在线段树上的表现则是对从根开始到相应叶子节点的一次修改,那么我们考虑保留原有线段树,每次修改新建一条路径,新线段树的根就是这条路径的起点.
原有的线段树
新建一条路径
新的线段树
如图所示,在对一个维护[1,10]序列的线段树进行修改(被修改的位置为7)时,我们新建了一条从根节点到[7,7]的路径,黄色边代表新建的点利用了原有的线段树的节点,红色的边代表路径上的边,我们可以用一份伪代码来描述这个过程:

void ins(原有线段树的根节点pre,新线段树的根节点rt,被修改的点的编号x,当前区间的左端点l,右端点r)
{
    新建一个节点,rt等于该节点//①
    修改操作
    if (x<=区间中点mid)
    {
        ins(pre的左儿子,rt的左儿子(此时它还是空的,在①处被新建),x,l,mid);
        rt的右儿子=pre的右儿子(利用原树节点)//②
    }
    else
    {
        ins(pre的右儿子,rt的右儿子,x,mid+1,r);
        rt的左儿子=pre的左儿子//②
    }
}

实际实现时可以一开始直接把pre赋值给rt,从而省去②操作

    void ins(int pre,int &rt,int x,int v,int l,int r)//给x处的数加上v
    {
        rt=++cnt;
        tr[rt]=tr[pre];//rt中存放着数的信息
        tr[rt].sum+=v;
        if (l>=r) return;
        if (x<=mid) ins(tr[pre].l,lson,x,v,l,mid);
        else ins(tr[pre].r,rson,x,v,mid+1,r);
    }

询问操作跟跟普通线段树没什么区别

    int query(int rt,int x,int l,int r)
    {
        if (l>x) return 0;
        if (r<=x) return tr[rt].sum;
        return query(lson,x,l,mid)+query(rson,x,mid+1,r);
    }
0x04带修改的可持久化线段树

类似于树状数组,每次询问和修改我们可以额外花费log()的代价,类似下面的操作

    void ins(LL x,LL v,LL key)
    {
        for(;x && x<=n;x+=lb(x))
            xg(bit[x],v,key,1,sz);
    }
    LL ask(LL x,LL k) //[1,x]<=k
    {
        LL res=0;
        for(;x;x-=lb(x))
            res+=query(bit[x],k,1,sz);
        return res;
    }
0x05注意事项

1.你可以把它想象成一个数组,它支持区间询问和单点修改,并支持访问所有历史版本.
因此合理安排修改顺序很关键.
2.注意空间大小,一不小心就会RE或者MLE.

0x06例题

1.
bzoj1901
裸的带修改主席树
代码
2.
bzoj3295
用带修改主席树维护每个前缀的权值线段树,删除时修改即可.
代码
3.
bzoj3932
裸的带修改线段树
代码
4.
bzoj2809
先搞出dfs序,每一个区间就是一个子树,然后枚举领导,主席树上二分能选到的最多人数.
5.
spoj_dquery
每个前缀维护一棵线段树,其中的节点i代表从i到当前点的颜色数目(假如颜色都不一样就是后缀和),每加一个点,last[col]代表它上一次出现的位置,若last[col]>0则在last[col]处减去1.
代码
6.
bzoj4771
先考虑没有深度限制的情况,可以用差分的方法,每个相同颜色的点处加1,在dfs序上相邻两个点lca处减1,那么每个子树的答案就是子树和.然后按深度顺序依次加点,就可以解决有深度限制的问题,对于每个x,d,在deep[x]+d深的子树里找即可.
代码

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页