阅读本文前,请确保已经阅读并理解了如下两篇文章:
『线段树 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] );
}

区间查询

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

『zkw线段树及其简单运用』-LMLPHP

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

『zkw线段树及其简单运用』-LMLPHP

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

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

『zkw线段树及其简单运用』-LMLPHP

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

『zkw线段树及其简单运用』-LMLPHP

然后让\(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\n",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("\n%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\n",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\n",query(k1,k2));
    }
}
int main(void)
{
    solve();
    return 0;
}


04-29 09:22