• sss


    <更新提示>

    <第一次更新>
    阅读本文前,请确保已经阅读并理解了如下两篇文章:
    『线段树 Segment Tree』
    『线段树简单运用』


    <正文>

    引入

    这是一种由(THU-zkw)大佬发明的数据结构,本质上是经典的线段树区间划分思想,采用了自底向上的方式传递区间信息,避免的递归结构,其代码相对经典线段树更简单,常数更小,易于实现。

    统计的力量-源自这里

    基础非递归

    接下来,我们将讲解(zkw)线段树的第一种实现形式,用于单点修改 区间查询,我们以查询区间最大值为例来讲解。

    建树

    普通线段树需要建树,(zkw)线段树当然也需要建树。

    考虑线段树的一个性质,其树上的叶节点代表的往往都是形如([x,x])的元区间,而且除最后一层外,线段树是一颗满二叉树,所以我们要把这颗线段树的数组大小先申请好了。

    一棵满二叉树有(x)个节点时,它有(frac{x+1}{2})个叶子节点,而我们需要至少(n)个叶子节点的线段树,即使(frac{x+1}{2}geq n),那么我们设(x=1),在(frac{x+1}{2}<n)时不断执行(x*=2),就能得到足够大小的线段树下标(base),由于线段树的叶子节点可能分布在两层,所以保险起见,我们还需再将(x)扩大一倍,即在(x+1<n)时不断执行(x*=2)就可以了。

    得到合适的下标位置后,将(1-n)下标位置的原数据直接存入线段树的叶子节点即可。

    其实,我们还需将下标再扩大两个位置,即需要保证(x>n),才停止执行(x*=2)。其原因是这样的:在执行区间查询操作时,我们需要将查询区间([l,r])更改为((l,r))(关于原因,我们之后再分析),才便于(zkw)线段树的查询,那么在询问([1,n])时,可能为调用到([0,n+1])的原下标,所以还需再扩大两个位置。

    得到了合适的下标(base)并将(1-n)的数据存入对应位置后,当然我们还要对(1)(base-1)的线段树位置进行区间更新,这个普通的更新就可以了。

    (Code:)

    inline void reset(void)
    {
        memset( val , 0 , sizeof val );
        base = 1;
    }
    inline void build(int *s,int len)
    {
        for (;base<=len;base<<=1);
        for (int i=base+1;i<=base+len;i++)
            val[i] = s[i-base];
        for (int i=base-1;i>=1;i--)
            val[i] = max( val[i<<1] , val[i<<1|1] );
    }
    

    单点修改

    直接在叶节点上修改对应的值,然后更新其每一个父节点即可。

    (Code:)

    inline void modify(int pos,int x)
    {
        pos += base;val[pos] = x;
        for (pos>>=1;pos;pos>>=1)
            val[pos] = max( val[pos<<1] , val[pos<<1|1] );
    }
    

    区间查询

    我们先来看一个最大值线段树。

    其中,叶节点下面的橙色代表数组上的原数值,淡蓝色代表线段树对应节点的区间最大值,棕色代表查询区间的范围,如图,我们需要查询区间([3,7])的最大值。

    enter image description here

    显然,我们只要查询上图带五角星的几个线段树节点的关键值,就能得知最大值。

    (zkw)线段树上,我们考虑如下一种方式:

    enter image description here

    先将闭区间([3,7])拓展为开区间((2,8)),我们设两个指针(l=2,r=8)

    enter image description here

    然后让(l,r)依次向上找父亲节点,直到两个节点(l,r)的父亲节点相同,我们停止向上查找。此时,结束位置的两个节点标记了粉色星,需要查询的节点还是标记的红色星。

    不难发现规律:当指针(l)经过的节点是一个左儿子时,或者当指针(r)经过的节点是一个右儿子时,它的兄弟就是一个需要查询的节点。

    对于一个查询,我们只需将闭区间转换为开区间,就能通过向上找父亲的遍历得到区间的答案,这就是使用开区间,并要求数组大小至少大于原大小两个位置的原因。

    (Code:)

    inline int query(int l,int r)
    {
        int res = 0;
        for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
        {
            if ( ~ l & 1 )res = max( res , val[l^1] );
            if ( r & 1 )res = max( res , val[r^1] );
        }
        return res;
    }
    

    至此,我们就能用极短的代码和很高的效率实现单点修改,区间查询的线段树了。

    (Code:)

    #include <bits/stdc++.h>
    using namespace std;
    const int N=100200;
    int val[N<<2],base,n,a[N];
    inline void read(int &k)
    {
        int x=0,w=0;char ch;
        while (!isdigit(ch))
            w |= ch=='-' , ch=getchar();
        while (isdigit(ch))
            x = x*10 + ch-48 , ch=getchar();
        k=(w?-x:x);return;
    }
    inline void reset(void)
    {
        memset( val , 0 , sizeof val );
        base = 1;
    }
    inline void build(int *s,int len)
    {
        for (;base<=len;base<<=1);
        for (int i=base+1;i<=base+len;i++)
            val[i] = s[i-base];
        for (int i=base-1;i>=1;i--)
            val[i] = max( val[i<<1] , val[i<<1|1] );
    }
    inline void modify(int pos,int x)
    {
        pos += base;val[pos] = x;
        for (pos>>=1;pos;pos>>=1)
            val[pos] = max( val[pos<<1] , val[pos<<1|1] );
    }
    inline int query(int l,int r)
    {
        int res = 0;
        for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
        {
            if ( ~ l & 1 )res = max( res , val[l^1] );
            if ( r & 1 )res = max( res , val[r^1] );
        }
        return res;
    }
    int main(void)
    {
        read(n);
        reset();
        build(a,n);
        for (int i=1;i<=n;i++)
        {
            int op,k1,k2;
            read(op);read(k1);read(k2);
            if (op==1)modify(k1,k2);
            if (op==2)printf("%d
    ",query(k1,k2));
        }
        return 0;
    }
    

    简单标记

    在此,我们要实现(zkw)线段树的第二种基本形式,用于区间修改 区间求和

    标记永久化

    对于区间修改 区间求和(zkw)线段树,最重要的思想就是标记永久化的思想。

    对于区间修改,我们在普通线段树上是通过(lazytag)的标记方式实现的,对于修改和查询操作调用到时,再下传标记。而在(zkw)线段树中,显然向下传递标记的方式是毫无用武之地了。那么,我们引入一种新的标记思想:标记永久化

    对于一个节点,若修改操作对节点所代表的整个区间产生影响,显然我们可以直接对该节点进行标记,而非逐层递归修改。那么,在自底向上的线段树中,我们可以不下传标记,而是在每一次查询时,统计累加一路上所有标记对答案产生的影响,这种标记思想被称为标记永久化

    建树

    该版本(zkw)线段树的建树方式和第一种形式的(zkw)线段树的建树方式一致,不再重复说明。

    (Code:)

    inline void build(void)
    {
        for (;base<=n;base<<=1);
        for (int i=base+1;i<=base+n;i++)
            val[i] = a[i-base];
        for (int i=base-1;i>=1;i--)
            val[i] =  val[i<<1] + val[i<<1|1] ;
    }
    

    区间修改

    关于标记永久化,我们进行定义:(add_i)代表线段树中(i)号节点的关键值已经进行修改,但是其所有子节点均有一个值为(add_i)的增量未进行处理

    我们采用上一版本(zkw)线段树区间查询的方式,设置两个开区间指针(l,r),并同时向上遍历。同时,我们维护三个变量(lcnt,rcnt,cnt),分别代表左指针处理增量的节点个数,右指针处理增量的节点个数,两个指针当前所在节点左包含的叶节点个数

    然后利用上述变量和(add)标记的定义,沿路更新(add)标记和原线段树即可,当然,对于(l,r)成为兄弟后,我们还须将(add)标记一直上推到根节点。

    (Code:)

    inline void modify(int l,int r,long long delta)
    {
        long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
        for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
        {
            val[l] += delta*lcnt;
            val[r] += delta*rcnt;
            if ( ~ l & 1 )
                add[l^1] += delta , val[l^1] += delta*cnt , lcnt += cnt;
            if ( r & 1 )
                add[r^1] += delta , val[r^1] += delta*cnt , rcnt += cnt;
        }
        for (; l || r ; l>>=1 , r>>=1 )
            val[l] += delta*lcnt , val[r] += delta*rcnt;
    }
    

    区间求和

    有了(add)标记,我们就很容易求得区间的和了。还是一样的方式,将闭区间转换为开区间,然后向上遍历,同样维护(lcnt,rcnt,cnt),然后利用(add)标记进行累加,再加上原来的区间和,就能得到答案。

    (Code:)

    inline long long query(int l,int r)
    {
        long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
        long long res = 0;
        for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
        {
            if (add[l]) res += add[l]*lcnt;
            if (add[r]) res += add[r]*rcnt;
            if ( ~ l & 1 )
                res += val[l^1] , lcnt += cnt;
            if ( r & 1 )
                res += val[r^1] , rcnt += cnt;
        }
        for (; l || r ; l>>=1 , r>>=1 )
            res += add[l]*lcnt , res += add[r]*rcnt;
        return res; 
    }
    

    至此,我们已经实现了支持区间修改,区间求和的(zkw)线段树了,对于更多需要维护求和性质的值,也可以使用标记永久化的思想,这需要读者理解掌握。

    (Code:)

    #include <bits/stdc++.h>
    using namespace std;
    const int N=100020;
    long long n,q,a[N],val[N<<2],base,add[N<<2];
    inline void reset(void)
    {
        memset( val , 0 , sizeof val );
        memset( add , 0 , sizeof add );
        base = 1;
    }
    inline void build(void)
    {
        for (;base<=n;base<<=1);
        for (int i=base+1;i<=base+n;i++)
            val[i] = a[i-base];
        for (int i=base-1;i>=1;i--)
            val[i] =  val[i<<1] + val[i<<1|1] ;
    }
    inline void modify(int l,int r,long long delta)
    {
        long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
        for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
        {
            val[l] += delta*lcnt;
            val[r] += delta*rcnt;
            if ( ~ l & 1 )
                add[l^1] += delta , val[l^1] += delta*cnt , lcnt += cnt;
            if ( r & 1 )
                add[r^1] += delta , val[r^1] += delta*cnt , rcnt += cnt;
        }
        for (; l || r ; l>>=1 , r>>=1 )
            val[l] += delta*lcnt , val[r] += delta*rcnt;
    }
    inline long long query(int l,int r)
    {
        long long lcnt = 0 , rcnt = 0 , cnt = 1 ;
        long long res = 0;
        for ( l=base+l-1 , r=base+r+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 , cnt<<=1 )
        {
            if (add[l]) res += add[l]*lcnt;
            if (add[r]) res += add[r]*rcnt;
            if ( ~ l & 1 )
                res += val[l^1] , lcnt += cnt;
            if ( r & 1 )
                res += val[r^1] , rcnt += cnt;
        }
        for (; l || r ; l>>=1 , r>>=1 )
            res += add[l]*lcnt , res += add[r]*rcnt;
        return res; 
    }
    inline void solve(void)
    {
        scanf("%lld%lld",&n,&q);
        for (int i=1;i<=n;i++)
            scanf("%lld",&a[i]);
        reset();
        build();
        for (int i=1;i<=q;i++)
        {
            char op;
            scanf("
    %c",&op);
            if (op=='C')
            {
                int l,r;long long delta;
                scanf("%d%d%lld",&l,&r,&delta);
                modify(l,r,delta);
            }
            if (op=='Q')
            {
                int l,r;
                scanf("%d%d",&l,&r);
                printf("%lld
    ",query(l,r));
            }
        }
    }
    int main(void)
    {
        freopen("b.in","r",stdin);
        freopen("b.out","w",stdout);
        solve();
        return 0;
    }
    

    差分思想和区间最值

    接下来,我们将要尝试实现使用区间查询的另一种形式,区间最值的查询。

    用上述两个模板稍微结合,更改一下难道不就可以实现区间修改,区间最值的(zkw)线段树了吗?答案是否定的。在区间修改的限制下,如果还用标记永久化的思想,由于标记的大小和位置未知,那么区间最值的查询就会出问题。

    差分思想

    现在,我们线段树上的节点将不再存对应区间的关键值了。我们需要用(zkw)线段树来维护原关键值的差分值,若原来的(val_i)代表节点(i)所代表区间的最大值,则现在我们需要维护的(val'_i=val_i-val_{i/2}),特殊地,(val_1)仍代表整个区间的最大值。

    可能读者已经发现一点性质了:从任意叶节点(y)开始,一直向上找父亲,并累加对应点的权值,就得到了原节点的权值。

    其实,我们还可以用这样的方式理解:(val_i)代表(i)节点所在区间的最大值比其父亲节点所在区间最大值大多少(可能负数)。

    建树

    还是可以利用和之前一样的方式建树,特殊地,在存完一个节点的值以后要利用(val_i)的定义来计算得到差分的值。

    (Code:)

    inline void build(void)
    {
        for (;base<=n;base<<=1);
        for (int i=base+1;i<=base+n;i++)
            val[i] = a[i-base];
        for (int i=base;i>=1;i--)
            val[i] = max( val[i<<1] , val[i<<1|1] ) ,
            val[i<<1] -= val[i] , val[i<<1|1] -= val[i];
    }
    

    区间修改

    有了差分线段树以后,我们发现区间修改就可以直接在树上操作了。还是利用开区间的方式,向上查找父亲并更新线段树,对于沿路访问到的每一个节点,由于可能其子树中包含修改过的节点,就要利用差分定义上传一下差值给父亲,就还能维护之前所提到的性质,而不用再去操作子节点。

    同样地,对于(l,r)指针成为兄弟后,还需将差值上推到根节点。

    (Code:)

    inline void modify(int l,int r,int delta)
    {
        int temp;
        for ( l=l+base-1 , r=r+base+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
        {
            if ( ~ l & 1 ) val[l^1] += delta;
            if ( r & 1 ) val[r^1] += delta;
            temp = max( val[l] , val[l^1] );
            val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
            temp = max( val[r] , val[r^1] );
            val[r] -= temp , val[r^1] -= temp , val[r>>1] += temp;
        }
        for (; l > 1 ; l>>=1 )
            temp = max( val[l] , val[l^1] ) ,
            val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
    }
    

    区间最值

    维护了这样一颗差分线段树,我们就可以用一种简单的方式来查询区间最值了。

    这次,我们维护(l,r)为闭区间的左右指针,在向上找父亲遍历的过程中,对左右指针遍历到节点的区间差分值取一下最大值,再一直向上累加,累加到根节点,就是区间最大值,这和单点向上累加的道理是一样的。

    (Code:)

    inline int query(int l,int r)
    {
        int lres = 0 ,rres = 0;
        l += base , r += base;
        if ( l ^ r )
        {
            for (; l ^ r ^ 1 ; l>>=1 , r>>=1 )
            {
                lres += val[l] , rres += val[r];
                if ( ~ l & 1 ) lres = max( lres , val[l^1] );
                if ( r & 1 ) rres = max( rres , val[r^1] );
            }
        }
        int res = max( lres + val[l] , rres + val[r] );
        while ( l > 1 ) res += val[l>>=1];
        return res;
    }
    

    这样,(zkw)线段树的三类基础模板就已经得到实现了,有关更多的拓展,需要我们灵活运用。

    (Code:)

    #include <bits/stdc++.h>
    using namespace std;
    const int N=100200;
    int val[N<<2],n,a[N],base;
    inline void read(int &k)
    {
        int x=0,w=0;char ch;
        while (!isdigit(ch))
            w |= ch=='-' , ch=getchar();
        while (isdigit(ch))
            x = x*10 + ch-48 , ch=getchar();
        k=(w?-x:x);return;
    }
    inline void reset(void)
    {
        memset( val , 0 , sizeof val );
        base = 1;
    }
    inline void build(void)
    {
        for (;base<=n;base<<=1);
        for (int i=base+1;i<=base+n;i++)
            val[i] = a[i-base];
        for (int i=base;i>=1;i--)
            val[i] = max( val[i<<1] , val[i<<1|1] ) ,
            val[i<<1] -= val[i] , val[i<<1|1] -= val[i];
    }
    inline void modify(int l,int r,int delta)
    {
        int temp;
        for ( l=l+base-1 , r=r+base+1 ; l ^ r ^ 1 ; l>>=1 , r>>=1 )
        {
            if ( ~ l & 1 ) val[l^1] += delta;
            if ( r & 1 ) val[r^1] += delta;
            temp = max( val[l] , val[l^1] );
            val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
            temp = max( val[r] , val[r^1] );
            val[r] -= temp , val[r^1] -= temp , val[r>>1] += temp;
        }
        for (; l > 1 ; l>>=1 )
            temp = max( val[l] , val[l^1] ) ,
            val[l] -= temp , val[l^1] -= temp , val[l>>1] += temp;
    }
    inline int query(int l,int r)
    {
        int lres = 0 ,rres = 0;
        l += base , r += base;
        if ( l ^ r )
        {
            for (; l ^ r ^ 1 ; l>>=1 , r>>=1 )
            {
                lres += val[l] , rres += val[r];
                if ( ~ l & 1 ) lres = max( lres , val[l^1] );
                if ( r & 1 ) rres = max( rres , val[r^1] );
            }
        }
        int res = max( lres + val[l] , rres + val[r] );
        while ( l > 1 ) res += val[l>>=1];
        return res;
    }
    inline void solve(void)
    {
        scanf("%d",&n);
        reset();
        build();
        for (int i=1;i<=n;i++)
        {
            int op,k1,k2;
            read(op),read(k1),read(k2);
            if (op==1)modify(k1,k2,1);
            if (op==2)printf("%d
    ",query(k1,k2));
        }
    }
    int main(void)
    {
        solve();
        return 0;
    }
    

    <后记>

  • 相关阅读:
    python设置环境变量(临时和永久)
    python items和setdefault函数
    Django学习day3——Django的简单使用
    DOS打印目录树到文件
    Django学习day2——Django安装与环境配置
    Django学习day1——Django的简单介绍
    sets,relations,and fuctions
    lecture 1
    number theory
    lecture 9.18
  • 原文地址:https://www.cnblogs.com/Parsnip/p/10785894.html
Copyright © 2020-2023  润新知