Pascal 线段树 lazy-tag 模板
先说下我的代码风格(很丑,勿喷)
maxn表示最大空间的四倍
tree数组表示求和的线段树
delta表示增减的增量标记
sign表示覆盖的标记
delta,sign实际上都是lazy标志
pushdown表示标记下传
pushup表示标记上传(即求和,区间最值)
update表示数据更新
线段树(segment tree)是一种特别有用的数据结构,我们在维护区间各种信息的时候它就是利器。可能读者嫌线段树代码太长,不想写,而树状数组代码简洁易于便携,但是我在这里想说,线段树能做到的很多东西树状数组无法做到,而若掌握了线段树的基本框架,再去写熟练的话,基本上能够做到较短的时间内DEBUG完。
我们来介绍一下什么是线段树。
问题1:
我们拥有一个数列,需要维护三个操作
1.单点修改,把a[i]的值改成v
2.区间修改,把a[i]~a[j]的值改成v
3.区间求和,求出a[i]~a[j]的和
我们不难想到使用数组存储这些东西,对于每个询问我们暴力修改,可这样的话,对于每一段区间,我们需要耗费的时间是r-l+1,当Q特别大、或者区间长度特别大的时候,时间无法忍受。这时候线段树的应用便出来了。
何谓线段树?我们把线段存储在树形结构中,利用树形结构维护线段信息的数据结构,就是线段树。
我们利用二分建树,建立一颗二叉树。为什么是二叉树呢?我个人理解是,这方便我们二分查找,二分递归,大大减少编程复杂度
而,我们每次需要的线段信息,只需要在线段树中检索, 线段树的实质是,一颗存储着线段信息的二叉搜索树。
我们把一个长的线段(区间),不断的二分划分,即[a,b]划分成[a,(a+b)>>1](leftchild),[(a+b)>>1+1,b](rightchild); 直到a=b为止。
我们把a=b的这个线段称为叶子节点。在某些题内,叶子节点存储的就是该点在线性结构所对应的信息。
由上可知,我们不断划分着[a,b]那么对于每一个[a,(a+b)>>1],[(a+b)>>1+1,b]它的左儿子和右儿子也是满足"线段树"性质
上图是我们划分[1,10]区间的过程。现在我们回到问题1
我们发现如果是一个一个叶子结点访问,那么每次访问的时间甚至比O(n)的数组模拟要长。
那么我们再看一个简单版的问题1
问题2:
给定一个序列
1.修改其中一个点
Sub:单点减小某值
Add:单点增加某值
2.询问区间和
Query:询问区间和
现在我们可以通过每次查找到叶子节点,暴力修改。
然后再递归求解区间和。
具体代码如下:
const maxn=300001; var tree:packed array [0..maxn] of longint; procedure pushup(now:longint); begin tree[now]:=tree[now<<1]+tree[now<<1+1]; //区间求和 end; procedure build(l,r,now:longint); var mid:longint; begin if l=r then begin read(tree[now]); //到叶子节点时输入信息并退出 exit; end; mid:=(l+r) shr 1; build(l,mid,now<<1); build(mid+1,r,now<<1+1); pushup(now); end; procedure update(p,add,l,r,now:longint); //更新数据 var mid:longint; begin if l=r then begin inc(tree[now],add); exit; end; mid:=(l+r) shr 1; if p<=mid then update(p,add,l,mid,now<<1) //递归求解 else update(p,add,mid+1,r,now<<1+1); pushup(now); end; function query(left,right,l,r,now:longint):longint; //询问和 var mid,ans:longint; begin if (left<=l) and (r<=right) then exit(tree[now]); mid:=(l+r) shr 1; ans:=0; if left<=mid then inc(ans,query(left,right,l,mid,now<<1)); if right>mid then inc(ans,query(left,right,mid+1,r,now<<1+1)); exit(ans); end; procedure ques(n:longint); var ch:char; a,b:longint; temp:string; begin while true do begin temp:=''; ch:='1'; while ch<>' ' do begin read(ch); temp:=temp+ch; end; delete(temp,length(temp),1); if temp='End' then exit; readln(a,b); case temp of 'Add':update(a,b,1,n,1); 'Query':writeln(query(a,b,1,n,1)); 'Sub':update(a,-b,1,n,1); end; end; end; procedure main; var i,t,n:longint; begin readln(t); for i:=1 to t do begin readln(n); build(1,n,1); readln; ques(n); end; end; begin main; end.
但是,我们如果对于区间有修改的话,这样就太慢了,O(Qnlogn),甚至比朴素算法都慢
那么,线段树的精华出现了,lazy-tag技术
Lazy-Tag 顾名思义,懒惰标记。我们每当要对一个区间进行修改时,
我们只需要访问到这个区间所包含的最大区间,然后对这个区间记一个lazy-tag
我们如此做的动机是:没有必要更新到叶子节点,而是在一个合适的范围内,上一个tag
而我们在询问的时候,每询问一个区间,查询其tag,若存在tag,则把tag下传到该区间的左儿子与右儿子,并把该区间的tag信息上传,且清空当前Tag。
这样我们能大大的增加效率。
pushdown应该如何写呢?
大概的结构是这样
if lazy[now]<>0 then
begin
do something;
end;
我们给出区间修改的问题3
维护一个数列,支持如下操作:
1.把区间[l,r]全部增加v
2.修改k点值为r
3.询问区间[l,r]的和
代码如下:(未写main函数)
//区间增减 区间求和 const maxn=600001; var lazy,tree:array [0..maxn] of longint; procedure pushup(now:longint); var left,right:longint; begin left:=now<<1; right:=left+1; tree[now]:=tree[left]+tree[right]; end; procedure pushdown(now,l,r:longint); var len,left,right,delta:longint; begin left:=now<<1; right:=left+1; delta:=lazy[now]; len:=r-l+1; if lazy[now]<>0 then begin inc(tree[now],delta*len); inc(lazy[left],delta); inc(lazy[right],delta); lazy[now]:=0; end; end; procedure build(l,r,now:longint); var mid,left,right:longint; begin mid:=(l+r)>>1; left:=now<<1; right:=left+1; lazy[now]:=0; if l=r then begin read(tree[now]); exit; end; build(l,mid,left); build(mid+1,r,right); pushup(now); end; procedure update(now,l,r:longint); var mid,left,right:longint; begin mid:=(l+r)>>1; left:=now<<1; right:=left+1; pushdown(left,l,mid); pushdown(right,mid+1,r); pushup(now); end; procedure insert(left,right,c,l,r,now:longint); var mid,leftch,rightch:longint; begin mid:=(l+r)>>1; leftch:=now<<1; rightch:=leftch+1; pushdown(now,l,r); if (left<=l) and (r<=right) then begin inc(lazy[now],c); exit; end; if left<=mid then insert(left,right,c,l,mid,leftch); if mid<right then insert(left,right,c,mid+1,r,rightch); update(now,l,r); end; function querysum(left,right,l,r,now:longint):longint; var leftch,rightch,mid,ans:longint; begin pushdown(now,l,r); if (left<=l) and (r<=right) then exit(tree[now]); mid:=(l+r) shr 1; leftch:=now<<1; rightch:=leftch+1; ans:=0; if left<=mid then inc(ans,querysum(left,right,l,mid,leftch)); if mid<right then inc(ans,querysum(left,right,mid+1,r,rightch)); exit(ans); end;
我们再升级一下问题
维护一个数列,支持如下操作:
0.询问区间[l,r]的和
1.修改单点l值为r
2.把区间[l,r]全部置为v
3.把区间[l,r]全部增加v
看上去很麻烦,我们不知道区间覆盖和区间增减的顺序的话,可能标记会出错。
但是我们只需要再维护一个sign域即可,而我们也不必在意区间覆盖和区间增减的顺序
因为:当区间覆盖与区间增量tag同时存在时,我们优先区间覆盖标记,在清空区间覆盖标记时,把区间增量标记清空
为什么呢?很显然,我们先进行区间增减,再进行区间覆盖,那么显然区间增减无意义。
但是问题来了,若我们先进行的区间覆盖,再进行区间增减,那么区间增减也会被覆盖掉,这样显然会WA.
怎么解决呢?我们在下tag时,都查询一次当前结点的tag,若当前结点存在tag,那么我们清空tag,使该层tag传递到下一层,
即当前更新不影响历史记录。
这样,我们能保持标记总是sign在上,不影响delta的传递,即覆盖标记永远优先于增量标记,且传递深度总是较增量标记传递深度大一层。
代码如下:(不支持全部覆盖为0,若要全部覆盖为0则需重新初始化sign的值)
{$inline on} const maxn=800001; var tree,sign,delta:array [0..maxn] of int64; procedure pushup(now:longint); inline; var left,right:longint; begin left:=now<<1; right:=left+1; tree[now]:=tree[left]+tree[right]; end; procedure pushdown(now,l,r:longint); inline; var left,right,len:longint; begin left:=now<<1; right:=left+1; len:=r-l+1; if sign[now]<>0 then begin sign[left]:=sign[now]; sign[right]:=sign[now]; delta[left]:=0; delta[right]:=0; tree[now]:=len*sign[now]; sign[now]:=0; end; if delta[now]<>0 then begin inc(delta[left],delta[now]); inc(delta[right],delta[now]); inc(tree[now],delta[now]*len); delta[now]:=0; end; end; procedure build(now,l,r:longint); inline; var mid,left,right:longint; begin mid:=(l+r)>>1; left:=now<<1; right:=left+1; delta[now]:=0; sign[now]:=0; if l=r then begin read(tree[now]); exit; end; build(left,l,mid); build(right,mid+1,r); pushup(now); end; procedure update(now,l,r:longint); inline; var mid,left,right:longint; begin mid:=(l+r)>>1; left:=now<<1; right:=left+1; pushdown(left,l,mid); pushdown(right,mid+1,r); pushup(now); end; procedure changepoint(pos,v,l,r,now:longint); inline; var mid,left,right:longint; begin mid:=(l+r)>>1; left:=now<<1; right:=left+1; pushdown(now,l,r); if l=r then begin tree[now]:=v; exit; end; if pos<=mid then changepoint(pos,v,l,mid,left) else changepoint(pos,v,mid+1,r,right); update(now,l,r); end; procedure change(left,right,v,l,r,now:longint); inline; var mid,leftch,rightch:longint; begin pushdown(now,l,r); mid:=(l+r)>>1; leftch:=now<<1; rightch:=leftch+1; if (left<=l) and (r<=right) then begin sign[now]:=v; delta[now]:=0; exit; end; if left<=mid then change(left,right,v,l,mid,leftch); if mid<right then change(left,right,v,mid+1,r,rightch); update(now,l,r); end; procedure insert(left,right,v,l,r,now:longint); inline; var mid,leftch,rightch:longint; begin mid:=(l+r)>>1; leftch:=now<<1; rightch:=leftch+1; pushdown(now,l,r); if (left<=l) and (r<=right) then begin inc(delta[now],v); exit; end; if left<=mid then insert(left,right,v,l,mid,leftch); if mid<right then insert(left,right,v,mid+1,r,rightch); update(now,l,r); end; function querysum(left,right,l,r,now:longint):int64; inline; var leftch,rightch,mid:longint; ans:int64; begin pushdown(now,l,r); if (left<=l) and (r<=right) then exit(tree[now]); mid:=(l+r)>>1; leftch:=now<<1; rightch:=leftch+1; ans:=0; if left<=mid then inc(ans,querysum(left,right,l,mid,leftch)); if mid<right then inc(ans,querysum(left,right,mid+1,r,rightch)); update(now,l,r); exit(ans); end; procedure swap(var l,r:longint); inline; begin l:=l xor r; r:=l xor r; l:=l xor r; end; procedure main; var i,n,m,que,l,r,a:longint; begin read(n,m); build(1,1,n); for i:=1 to m do begin read(que,l,r); if l>r then swap(l,r); case que of 0:writeln(querysum(l,r,1,n,1)); 1:changepoint(l,r,1,n,1); 2: begin read(a); change(l,r,a,1,n,1); end; 3: begin read(a); insert(l,r,a,1,n,1); end; end; end; end; begin main; end.
另外:
线段树一定要用数组,指针常数巨大。
下面是指针与数组版本的耗时比较
此为指针版,耗时耗空间都相当大(不排除本人蒟蒻原因写丑了)
此为数组版,常数还是比较小的。