thewalker88梦游仙境——主席树详解(静态主席树,带修改主席树,树上主席树)

主席树的微不足道的小结

前言
我弱的很啊,最近学习了主席树这个高级数据结构,真的是想了好久,本来懂了点结果带修改的又给咱搞懵了。。。于是想写一篇博客总结一下,不敢说能对大家有帮助,主要是与大家交流姿势,提高姿势水平。有不认同的地方,欢迎大家讨论。
1.初识主席树
蒟蒻thewalker88在狂风中彳亍着,他又冷又饿,渴望一个温暖的地方。
他走啊走,忽得一山,山有小口,仿佛若有光,便从口入,初极狭,才通人,复行数十步,豁然开朗。
他看见在阳光的照耀下,一个长者,穿着西服,裤子提的很高,戴着一副黑框眼镜,正努力的种树。那棵树好奇怪,像是好几颗线段树纠缠在一起似的。
walker震惊了,他这个蒟蒻从没见过这种树。他向那个长者走去:“请问,您种的是什么树啊。”长者微微一笑,笑容中充满了和蔼:“这是主席树。”
thewalker88从没听说过这种树,便虚心的向长者求教。
长者又笑了:“你们年轻人啊,还需要提高自己的姿势水平啊。我就给你讲讲这主席树吧。”
“不要被主席树的名号吓到,它是个很森破的数据结构。你比如bzoj3524,频繁询问区间第k小元素是什么,这个问题如果我们用权值线段树解决,就需要离线处理,但你如果用主席树,就可以性感题目,在线询问了。这是坠吼的。
”主席树是什么呢,她是利用函数式编程的思想来使线段树支持询问历史版本、同时充分利用它们之间的共同数据来减少时间和空间消耗的酷炫无敌叼炸天的堪比scp-682的存在的线段树。

函数式编程
简单说,”函数式编程”是一种”编程范式”(programming paradigm),也就是如何编写程序的方法论。
它属于”结构化编程”的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。
—–百度百科

“主席树的每一个节点对应一颗主席树,她是对于一个长度为n的序列的每个前缀(共n个)建立一颗权值线段树,第i个主席树存储的是原序列第一到第i个数中属于权值区间L到R的数有几个。(努力理解理解)。
然而thewalker88太弱了,根本听不懂,长者又一笑,仿佛在暗自嘲笑walker的奈亦乌。
长者当然文武双全啦,于是他给蒟蒻thewalker88画了几幅图:
thewalker88梦游仙境——主席树详解(静态主席树,带修改主席树,树上主席树)_第1张图片
”我懂了!我这就去水题!”thewalker88还是图样图森破,兴冲冲的就要走。
“啊呀,年轻人,别老想搞个大新闻,要闷声发大财,你这么写,会出现什么问题滋道吗?”
thewalker88笑容渐渐消失“会。。。会MLE。”
长者又和蔼地笑了“对啦,所以要怎么优化呢?”
“我们注意到,每次新加入一个数后,我们只有一条链的值会被修改,如我们在刚才的例子中,我们插入一个3,我们只有“1~4”->”3~4”->”3~3”这一条链会发生改变,大部分的值都是相同的,因此主席树的精髓在于:修改一个值的时候,我们只需要修改logn个节点,因此我们只新开logn个节点,其余大部分值相同的节点,我们共用,因此,真正的主席树,是这样的!
thewalker88梦游仙境——主席树详解(静态主席树,带修改主席树,树上主席树)_第2张图片
“我这个主席树有一个好,有问题时跑起来比划分树都快。滋磁多次询问区间第k小,而且按照基本法,不会MLE,这是坠吼滴。每次要离散化一下,然后每插入一个数,判断接下来应该搞左儿子还是右儿子,若左儿子不发生变化,就钦点她上一颗树的左儿子当下任,右儿子不发生变化同理。
长者的代码

int n,m,sz;
int root[500010],ls[10000010],rs[10000010],sum[10000010];
inline void update(int l,int r,int x,int &y,int v)//y是当前的树的节点,x是上一颗树的节点,l,r是权值区间
{
    y=++sz;//给y赋值
    sum[y]=sum[x]+1;//主席树的精髓就是利用上一颗树的状态来更新自己的状态
    if(l==r)return ;//到叶子节点了
    ls[y]=ls[x];rs[y]=rs[x];//我先钦定这棵树和上一棵树左右儿子相同,接下来再修改
    int mid=(l+r)>>1;
    if(v<=mid)update(l,mid,ls[x],ls[y],v);
    else update(mid+1,r,rs[x],rs[y],v);
}

”这就是主席树的建树,刚开始的主席树是空的(抱歉没画orz),然后每插入一个数,更新一次。“
thewalker88懂了!他好开心!他说道:”感觉主席树和前缀和之类的东西好像啊(请你们原谅他,蒟蒻就是有这么丰富的联想能力)“
是的!主席树与前缀和之类的东西有十分相似的性质!联想前缀和数组的话,便于理解主席树。“长者笑眯眯地说。
”对于前缀和数组,由于她们形式相同,通过相减的方法能够提取出一段区间的和。而每一颗主席树的形态也都是相同的,类比得出结论:通过让第r颗树减去第l颗树,我们可以提取出区间l到r的信息!,因此我们就可以询问区间第k小(大)了!”
“我们设左儿子有cnt个数,如果cnt>k,就搞左儿子,否则在右儿子中询问第k-cnt小的数,一个简单的线段树询问。”
长者说道:“bzoj3524和bzoj2223都是主席树裸题,一模一样,双倍经验坠吼滴(但有一个小坑,考验你对主席树是否理解到位)。“
3524代码

#include
#include
#include
#include
#include
#include
#include

using namespace std;

inline int read()
{
    int s=0,w=1;
    char ch=getchar();
    while(ch<'0'||ch>'9')
    {
        if(ch=='-')
        {
            w=-1;
        }
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        s=s*10+ch-'0';
        ch=getchar();
    }
    return s*w;
}
inline void print(int x)
{
    if(x<0)
    {
        x=-x;
        putchar('-');
    }
    if(x>=10)
    {
        print(x/10);
    }
    putchar(x%10+'0');
}
int n,m,sz;
int root[500010],ls[10000010],rs[10000010],sum[10000010];
inline void update(int l,int r,int x,int &y,int v)
{
    y=++sz;
    sum[y]=sum[x]+1;
    if(l==r)return ;
    ls[y]=ls[x];rs[y]=rs[x];
    int mid=(l+r)>>1;
    if(v<=mid)update(l,mid,ls[x],ls[y],v);
    else update(mid+1,r,rs[x],rs[y],v);
}
inline int que(int L,int R)
{
    int l=1,r=n,mid,x,y,tmp=(R-L+1)>>1;
    x=root[L-1];y=root[R];
    while(l!=r)
    {

        if(sum[y]-sum[x]<=tmp)
        {
            return 0;
        }
        mid=(l+r)>>1;
        if(sum[ls[y]]-sum[ls[x]]>tmp)
        {
            r=mid;
            x=ls[x];
            y=ls[y];
        }
        else if(sum[rs[y]]-sum[rs[x]]>tmp)
        {
            l=mid+1;
            x=rs[x];
            y=rs[y];
        }
        else return 0;
    }
    return l;
}
int main()
{
    n=read();m=read();
    for(int i=1;i<=n;i++)
    {
        int x;
        x=read();
        update(1,n,root[i-1],root[i],x);
    }
    for(int i=1;i<=m;i++)
    {
        int l,r;
        l=read();r=read();
        print(que(l,r));
        putchar(10);
    }
}

(静态主席树讲完啦QwQ)
2.带修改主席树
thewalker88听完很是兴奋,渐渐有了一个大胆的想法,但是他发现了一个问题,如果这个序列需要维护修改权值的操作怎么办呢???总不能每棵树修改一遍吧。
长者慈祥的提提裤腰带,推推眼镜:”你不觉得,这个问题很熟悉吗??
(思考时间QwQ)
(思考时间到辣QwQ)
thewalker88灵光乍现:”好像我们没有树状数组时,修改前缀和的困惑!“
长者开心的笑了,好像变的年轻了:”对啦,对于前缀和,我们用了树状数组这个优雅的数据结构来使修改简化,既然我们说过主席树与前缀和很像,那么我们对于主席树,修改时,也用树状数组维护!
”用树状数组维护时,每颗主席树维护的原序列的区间要发生改变(联想树状数组和前缀和数组)(作者注:为什么。。。为什么我看的所有博客没人说这么关键的话啊啊啊啊啊,这句话很重要啊啊啊啊,这样才能理解啊啊啊啊),第i颗主席树维护长度为lowbit(i)的区间的信息。
“修改时和询问时,我们只需要跳lowbit,求的要操作的树是哪些。大概两种写法,一种是用一个cur[ ]来存一下你当前走到哪棵树了,二是用L[ ]和R[ ]来存储需要操作的树,然后再操作。注意!第一种写法好像不正确!
”bzoj1901是带修改主席树裸题,你试试吧。“
bzoj1901代码:

    #include
#include
#include
#include
#include
#include
#include

using namespace std;

#define MAXN 2200005
#define lowbit(x) x&(-x)
inline int read()
{
    int s=0,w=1;
    char ch=getchar();
    while(ch<'0'||ch>'9')
    {
        if(ch=='-')
        {
            w=-1;
        }
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        s=s*10+ch-'0';
        ch=getchar();
    }
    return s*w;
}
inline void print(int x)
{
    if(x<0)
    {
        x=-x;
        putchar('-');
    }
    if(x>=10)
    {
        print(x/10);
    }
    putchar(x%10+'0');
}
int n,m,tot,top,sz;
int v[10001],num[20001],Hash[20001];
int flag[10001],A[10001],B[10001],K[10001],root[10001];
int sum[MAXN],ls[MAXN],rs[MAXN];
int L[30],R[30],a,b;
inline int get(int x)
{
    int l=1,r=tot,mid;
    while(l<=r)
    {
        mid=(l+r)>>1;
        if(Hash[mid]1;
            else r=mid-1;
    }
    return l;
}
inline void update(int &rt,int l,int r,int x,int w)
{
    if(!rt)rt=++sz;
    sum[rt]+=x;
    if(l>=r)return;
    int mid=(l+r)>>1;
    if(w<=mid)update(ls[rt],l,mid,x,w);
        else update(rs[rt],mid+1,r,x,w);
}
inline int que(int l,int r,int k)
{
    if(l==r)return l;
    int suml=0,sumr=0;
    for(int i=1;i<=a;i++)suml+=sum[ls[L[i]]];
    for(int i=1;i<=b;i++)sumr+=sum[ls[R[i]]];
    int mid=(l+r)>>1;
    if(sumr-suml>=k)
    {
        for(int i=1;i<=a;i++)L[i]=ls[L[i]];
        for(int i=1;i<=b;i++)R[i]=ls[R[i]];
        return que(l,mid,k);
    }
    else
    {
        for(int i=1;i<=a;i++)L[i]=rs[L[i]];
        for(int i=1;i<=b;i++)R[i]=rs[R[i]];
        return que(mid+1,r,k-(sumr-suml));
    }
}
int main()
{
    char s[3];
    n=read();m=read();
    for(int i=1;i<=n;i++)
    {
        v[i]=read();
        num[++top]=v[i];
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%s",s);
        A[i]=read();B[i]=read();
        if(s[0]=='Q')
        {
            K[i]=read();
            flag[i]=1;
        }
        else num[++top]=B[i];
    }
    sort(num+1,num+top+1);
    Hash[++tot]=num[1];
    for(int i=2;i<=top;i++)
    {
        if(num[i]!=num[i-1])
        {
            Hash[++tot]=num[i];
        }
    }
    for(int i=1;i<=n;i++)
    {
        int t=get(v[i]);
        for(int j=i;j<=n;j+=lowbit(j))
        {
            update(root[j],1,tot,1,t);
        }
    }
    for(int i=1;i<=m;i++)
    {
        if(flag[i])
        {
            a=0;b=0;A[i]--;
            for(int j=A[i];j>0;j-=lowbit(j))
            {
                L[++a]=root[j];//存储需要操作的树
            }
            for(int j=B[i];j>0;j-=lowbit(j))
            {
                R[++b]=root[j];
            }
            print(Hash[que(1,tot,K[i])]);
            putchar(10);
        }
        else
        {
            int t=get(v[A[i]]);
            for(int j=A[i];j<=n;j+=lowbit(j))
            {
                update(root[j],1,tot,-1,t);
            }
            v[A[i]]=B[i];
            t=get(B[i]);
            for(int j=A[i];j<=n;j+=lowbit(j))
            {
                update(root[j],1,tot,1,t);
            }
        }
    }
    return 0;
}

3.树上主席树
thewalker88掌握了这个高级数据结构,激动极了,长者见他如此激动 ,便说:”怎样,主席树是不是很一颗赛艇,我出个问题考考你:如果是树上主席树,我每棵树该存什么,又如何提取出u到v的一条简单路径上的信息呢?“
thewalker88懵逼了!
他想问问长者,可长者摇了摇头:”无可奉告“
thewalker88想了想,决定拿出在千古神犇酷炫无敌叼炸天的吊打scp-682的天神下凡的至尊Starria的课上的口胡能力来口胡一番。
”我们先搞出她的dfs序(可以用树链剖分搞),然后对于每颗主席树,我们存她代表的点到树的根节点的路径上的信息,至于提取路径u到路径v吗。。。。只需用sum[u]+sum[v]-sum[ lca(u,v) ]-sum[ fa[ lca(u,v) ] ]就能提取出来这条路径上的信息了。”
长者开心的笑了:”你说的没错!中央已经决定了,主席树你学会了!“说罢,伸出两个大拇指。

最后长者语重心长的说道:”主席树这个东西吼啊,我看见她时当场念了两句诗,你们年轻oier啊,要努力提高姿势水平,不能太奈亦乌,识得唔识得啊。“thewalker88按捺不住自己好奇的心:”您到底是谁?“长者微微一笑:”你的闹钟响了。“

说完,thewalker88就醒来了。他无论怎么想,都记不起来长者的模样,但在他的脑海里挥之不去的,除了主席树,还有一副黑框眼镜。。。。。

thewalker88思前想后,不敢怠慢,把这次经历记录下来,与诸君共赏。
5.后记
放飞自我的一篇博客QWQ。
如有错误,欢迎来骂。
也可加咱qq啊:1145101354(请注明在csdn上看见的博客谢谢QwQ)
最后祝您,身体健康(提乾涉经)。
~~~~~~~

你可能感兴趣的:(thewalker88梦游仙境——主席树详解(静态主席树,带修改主席树,树上主席树))