zkw 线段树学习笔记

一、简介

zkw 线段树专门用于线段树卡常,同时码量比普通线段树要小。

原理是通过将线段树补成完全二叉树,直接找到第 \(i\) 个叶子节点(编号为 \(p+i\) ),然后从下往上更新,从而避免递归

这里常数 p=1<<(__lg(n)+1) ,编号为 \(p\)\(p+n+1\) 的叶子为虚点,编号为 \(p+1\sim p+n\) 的叶子允许访问,编号大于 \(p+n+1\) 的节点不会被访问到,所以只需要开三倍空间

二、操作

时间复杂度同普通线段树,后面不再单独分析。

初始化

一口气把信息读到叶子上,然后从下往上更新即可。

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

注意从 (p+n)>>1 开始更新是为了防止越界。

单点修改,区间查询

先解决单点修改的问题。如果信息有可加性,一路修改即可。

void modify(int x,int v)
{
    for(x+=p;x>=1;x>>=1) sum[p]+=v;
}

如果信息没有可加性,单点修改然后一路 pushup

void modify(int x,int v)
{
    x+=p,sum[p]+=v;
    for(;x!=1;x>>=1) pushup(x>>1);
}

再解决区间查询的问题。线段树和 \(\texttt{01trie}\) 本质上是一回事,我们的目标是把区间 \([l,r]\) 拆成 \(\texttt{01trie}\) 上的 \(\mathcal O(\log n)\) 棵子树。

首先将 \([l,r]\) 转化为 \((l-1,r+1)\) ,然后让左右端点同步往上跳。

如果左端点是其父节点的左儿子,那么右子树一定完全包含在区间中。

如果右端点是其父节点的右儿子,那么左子树一定完全包含在区间中。

跳到左右端点互为兄弟节点时,循环结束。

ll query(int l,int r)
{
    ll res=0;
    for(l=p+l-1,r=p+r+1;l^r^1;l>>=1,r>>=1)
    {
        if(~l&1) res+=sum[l^1];
        if(r&1) res+=sum[r^1];
    }
    return res;
}

区间修改,单点查询

同普通线段树,我们需要引入懒标记。

由于只涉及到单点查询,我们不需要处理信息与信息的合并,甚至可以只用一个数组同时存储标记(非叶节点)和信息(叶节点)!

void modify(int l,int r,int v)
{
    for(l=p+l-1,r=p+r+1;l^r^1;l>>=1,r>>=1)
    {
        if(~l&1) sum[l^1]+=v;
        if(r&1) sum[r^1]+=v;
    }
}
ll query(int x)
{
    ll res=0;
    for(x+=p;x;x>>=1) res+=sum[x];
    return res;
}

区间修改,区间查询

这需要我们对懒标记有足够深刻的理解:

线段树要求:标记与标记可合并,标记与信息可合并,信息与信息可合并。

标记与信息的关系:

  • 节点 \(p\) 的标记打给 \(p\) 的整棵子树(不包含 \(p\) 自身),标记对 \(p\) 的影响已经体现在 \(val_p\) 中,叶节点的标记无意义。
  • 节点 \(p\) 的信息 \(val_p\) 表示考虑 \(p\) 子树内(包含 \(p\) 自身)的所有标记时,第 \([l_p,r_p]\) 个叶子的信息之和。

标记永久化

普通线段树查询方法:收集 \(1\to fa_p\) 的标记,若节点 \(p\) 对应的区间 \([l_p,r_p]\) 完全包含于查询区间 \([l,r]\) ,计算节点 \(p\) 的贡献。

标记永久化一般要求标记和信息都满足交换律。

我们需要对 \((l-1,r+1)\) 拆出来的 \(\mathcal O(\log n)\) 个区间更新信息并打上标记,同时左右端点的信息也要更新,因为它与 \([l,r]\) 有交叉。

拆区间的过程中我们跳到左右端点为兄弟节点时就停下了,接下来还要一路更新到根。

以区间加为例,维护左右端点对应的区间与修改区间 \([l,r]\) 的交集长度 \(lcnt,rcnt\) 即可。

void modify(int l,int r,int v)
{
    ll lcnt=0,rcnt=0,cnt=1;
    for(l=p+l-1,r=p+r+1;l^r^1;l>>=1,r>>=1,cnt<<=1)
    {
        sum[l]+=v*lcnt,sum[r]+=v*rcnt;
        if(~l&1) add[l^1]+=v,sum[l^1]+=v*cnt,lcnt+=cnt;
        if(r&1) add[r^1]+=v,sum[r^1]+=v*cnt,rcnt+=cnt;
    }
    for(;l;l>>=1,r>>=1) sum[l]+=v*lcnt,sum[r]+=v*rcnt;
}

那如果是区间覆盖这种不满足交换律的标记呢?

维护标记的时间戳也能做,让时间戳较大的标记覆盖较小标记,代码懒得写了。


区间查询的步骤和区间修改大致相同,别忘了最后要跳到根。

ll query(int l,int r)
{
    ll lcnt=0,rcnt=0,cnt=1,res=0;
    for(l=p+l-1,r=p+r+1;l^r^1;l>>=1,r>>=1,cnt<<=1)
    {
        res+=add[l]*lcnt+add[r]*rcnt;
        if(~l&1) res+=sum[l^1],lcnt+=cnt;
        if(r&1) res+=sum[r^1],rcnt+=cnt;
    }
    for(;l;l>>=1,r>>=1) res+=add[l]*lcnt+add[r]*rcnt;
    return res;
}

个人感觉标记永久化的用途没有标记下传广,而且理解起来难度较大,建议能用标记下传就用标记下传

标记下传

普通线段树查询方法:

  • 如果 \([l_p,r_p]\subseteq[l,r]\) ,统计节点 \(p\) 的信息。
  • 如果 \([l_p,r_p]\cap[l,r]=\varnothing\) ,直接返回。
  • 下传标记后分别统计左右二字的信息。

注:从上述过程也可看出叶节点的标记无意义。

以区间加区间求和为例,为方便下传标记,我们需要记录每个节点代表区间的左右端点,在初始化时自底向上更新即可。

注意虚点 \(p+n+1\) 的祖先节点由于右端点没有定义,所以维护的信息是错误的。但是它们维护的标记是正确的,拆区间也不会用到它们的信息。

struct node
{
    int l,r;
    ll add,sum;
}f[3*maxn];
void build()
{
    for(int i=0;i<=n+1;i++) f[p+i]={i,i,0,a[i]};
    for(int i=(p+n)>>1;i>=1;i--) f[i]={f[ls].l,f[rs].r,0,f[ls].sum+f[rs].sum};
}

如果维护的是区间加区间最值等操作,那么没有必要维护左右端点,代码可以更加简洁。


记录线段树深度 \(dep\) ,对 \(p+l-1\)\(p+r+1\) 的祖先节点从上往下 pushdown ,然后正常拆区间即可。

注意拆区间时需要同步 pushup ,并且最后要 pushup 到根节点。

void modify(int l,int r,ll v)
{
    static int dep=__lg(p);
    l=p+l-1,r=p+r+1;
    for(int i=dep;i>=1;i--) pushdown(l>>i),pushdown(r>>i);
    for(;l^r^1;l>>=1,r>>=1)
    {
        if(~l&1) pushadd(l^1,v);
        if(r&1) pushadd(r^1,v);
        pushup(l>>1),pushup(r>>1);
    }
    for(;l!=1;l>>=1) pushup(l>>1);
}

区间查询除了先 pushdown 以外没啥其他变化。

ll query(int l,int r)
{
    static int dep=__lg(p);
    l=p+l-1,r=p+r+1;
    for(int i=dep;i>=1;i--) pushdown(l>>i),pushdown(r>>i);
    ll res=0;
    for(;l^r^1;l>>=1,r>>=1)
    {
        if(~l&1) res+=f[l^1].sum;
        if(r&1) res+=f[r^1].sum;
    }
    return res;
}

三、效率对比

zkw 线段树主要卡的是在树上遍历的时间,如果瓶颈在维护信息(如矩阵)那么收效甚微。

\(\texttt{LOJ130}\) :单点修改,区间查询, \(n=q=10^6\)

  • 树状数组:代码\(\texttt{511B,270ms}\)
  • zkw 线段树:代码\(\texttt{768B,256ms}\)
  • 普通线段树:代码\(\texttt{1010B,515ms}\)

\(\texttt{LOJ131}\) :区间修改,单点查询, \(n=q=10^6\)

  • 树状数组:代码\(\texttt{554B,287ms}\)
  • zkw 线段树:代码\(\texttt{642B,281ms}\)
  • 普通线段树:代码\(\texttt{1232B,708ms}\)

\(\texttt{LOJ132}\) :区间修改,区间查询, \(n=q=10^6\)

  • 树状数组:代码\(\texttt{760B,426ms}\)
  • 标记永久化 zkw 线段树:代码\(\texttt{1203B,392ms}\)
  • 标记下传 zkw 线段树:代码\(\texttt{1452B,600ms}\)
  • 普通线段树:代码\(\texttt{1251B,855ms}\)
posted @ 2024-10-22 15:39  peiwenjun  阅读(7)  评论(0编辑  收藏  举报