在线段树的“区间查询”指令中,每当遇到被询问区间[l,r]完全覆盖当前节点时,我们知道线段树每个节点其实是代表一个小区间,也就是l<A[p].l<A[p].r<r。
我们已经证明,被询问区间[l,r]在线段树上被分成log N个小区间(节点),从而在O(log N)时间内求出答案。
不过,在“区间修改”指令中,如果某个节点被修改区间[l,r]完全覆盖,那么以该节点为根的整棵树中存储信息都会发生改变,若逐一进行更新,将使得一次区间修改指令的时间复杂度增加到O(N),多次询问时M * O(N)。M,N数量级很大时,M * O(N)的时间复杂度是我们不能接受的。
我们试想一下,多次修改后,到最后P节点代表的区间[Pl,Pr]被修改区间[l,r]完全覆盖,并且逐一更新子树P中所有节点,但在之后的查询指令中根本就没有用到,那么更新p的整棵子树就是徒劳的。
这就给我一个优化提示:
我们能不能碰到完全覆盖区间,不马上修改子树,只是在这个节点上做个变化标记,就立即返回。在后续指令中,需要从p向下递归,我们检查p节点是否有标记。若有,更新p的两个子节点,同时为p的两个子节点增加标记,然后清除p的标记。
向节点p增加一个标记,其实就是标识“该节点曾经被修改过,但其子节点尚未被更新”。
这样一来,每次查询和修改指令都会变为O(log N)。
我们还是用树状数组的一个例子:
A Simple Problem with Integers POJ3468
这个题的题干查看A Tiny Problem with Integers(树状数组区间增加、单点查询)博文
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int SIZE=100010;
struct SegmentTree{
int l,r;
long long sum,add;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define sum(x) tree[x].sum
#define add(x) tree[x].add
}tree[SIZE*4];
int a[SIZE],n,m;
void build(int p,int l,int r)//No.p, [l,r]
{
l(p)=l,r(p)=r;
if(l==r) { sum(p)=a[l]; return; }
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
sum(p)=sum(p*2)+sum(p*2+1);
}
void spread(int p)
{
if(add(p))
{
sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1);
sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1);
add(p*2)+=add(p);
add(p*2+1)+=add(p);
add(p)=0;
}
}
void change(int p,int l,int r,int z)
{
if(l<=l(p)&&r>=r(p))
{
sum(p)+=(long long)z*(r(p)-l(p)+1);
add(p)+=z;
return;
}
spread(p);
int mid=(l(p)+r(p))/2;
if(l<=mid) change(p*2,l,r,z);
if(r>mid) change(p*2+1,l,r,z);
sum(p)=sum(p*2)+sum(p*2+1);
}
long long ask(int p,int l,int r)
{
if(l<=l(p)&&r>=r(p)) return sum(p);
spread(p);
int mid=(l(p)+r(p))/2;
long long ans=0;
if(l<=mid) ans+=ask(p*2,l,r);
if(r>mid) ans+=ask(p*2+1,l,r);
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
build(1,1,n);
while(m--)
{
char op[2]; int x,y,z;
scanf("%s%d%d",op,&x,&y);
if(op[0]=='C')
{
scanf("%d",&z);
change(1,x,y,z);
}
else printf("%I64d\n",ask(1,x,y));
}
}