前文我们讲了树状数组与线段树基本的实现,本文我们讲线段树的一些优化。
动态开点线段树
我们知道,如果使用二叉树实现,需要给线段树开大小为 4n 的数组。为了节省空间,我们可以不一次性建好树,而是在最初只建立一个根结点代表整个区间。当我们需要访问某个子区间时,才建立代表这个区间的子结点。我们用ls
和rs
记录儿子的编号。(结点只有在需要的时候才被创建)
时间复杂度仍为 O(logn)
经过 m 次操作后,节点的数量规模为 O(mlogn),并且最多也只会创建 2n−1 个节点,没有浪费。
区间修改
1 2 3 4 5 6 7 8 9 10 11 12 13
| void modify(int& o,int l,int r){ if(!o)o=++cnt; if(ql<=l&&r<=qr){ t[o] += qk*(r-l+1); add[o] += qk; return; } pushdown(o,l,r); int mid = (l+r)>>1; if(ql<=mid)modify(ls[o],l,mid); if(qr>mid)modify(rs[o],mid+1,r); t[o] = t[rs[o]]+t[ls[o]]; }
|
区间查询
1 2 3 4 5 6 7 8 9 10 11 12
| ll query(int o,int l,int r){ if(!o)return 0; if(ql<=l&&r<=qr){ return t[o]; } pushdown(o,l,r); int mid = (l+r)>>1; ll ans=0; if(ql<=mid)ans+=query(ls[o],l,mid); if(qr>mid)ans+=query(rs[o],mid+1,r); return ans; }
|
pushdown操作
1 2 3 4 5 6 7 8 9 10 11 12
| void pushdown(int o,int l,int r){ int mid = (l+r)>>1; if(!ls[o])ls[o]=++cnt; if(!rs[o])rs[o]=++cnt; int lch=ls[o],rch=rs[o];
t[lch] += add[o] * (mid-l+1); t[rch] += add[o] * (r-mid); add[lch] += add[o]; add[rch] += add[o]; add[o]=0; }
|
标记永久化
如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。
这样,在做查询时,只需增加一个变量,记录从根节点到当前节点的标记总和即可。
1 2 3 4 5 6 7 8 9 10 11 12
| ll query(int o,int l,int r,ll lz){ if(ql<=l&&r<=qr){ return t[o] + lz*(r-l+1); } pushdown(o,l,r); int mid = (l+r)>>1; ll ans=0; int lch=o<<1,rch=(o<<1)|1; if(ql<=mid)ans+=query(lch,l,mid,lz+add[o]); if(qr>mid)ans+=query(rch,mid+1,r,lz+add[o]); return ans; }
|