在之前整理树状数组笔记时,已经将单点修改以及区间查询写的很清楚了。树状数组本质上就是一个可以在线 快速查询前缀和,并可以快速更新数值并维护的数据结构。我们喜欢用树状数组是因为其代码简单易记,出错率低,并且效率还不错。
如果涉及单点修改、区间查询,那么代码很简单。但是如果是区间修改,就需要做一些改变了。首先进行如下推导:(前置知识:前缀和、差分数组)
假设数组a是存放初始数据,sum存放a数组的前缀和,而b数组为差分数组,即每当为[l,r]位置所有元素+val时,b[l]+= val,b[r] -= val。而sumb为b数组的前缀和数组,这样sumb[i]就存放着a[i]修改值。那么很显然最后a[i]的值为a[i]+sumb[i],即初始值+修改值。此时再来考察a数组的前缀和,sum[i]存放的是a[1]+a[2]+…+a[i],(请注意a数组始终未作改变,其改变的值存放在sumb数组中),很显然我们要求的结果(数组a的最终前缀和)
a n s = s u m [ i ] + ∑ j = 1 i s u m b [ j ] ans = sum[ i ]+\sum_{j = 1}^i sumb[ j ] ans=sum[i]+j=1∑isumb[j]
又由于
s u m b [ i ] = ∑ j = 1 i b [ j ] sumb[i] = \sum_{ j = 1}^i b[j] sumb[i]=j=1∑ib[j]
所以
a n s = s u m [ i ] + ∑ j = 1 i ∑ k = 1 j b [ k ] ans = sum[i] + \sum_{j = 1}^i \sum_{k = 1}^j b[k] ans=sum[i]+j=1∑ik=1∑jb[k]
至此我们已经抛弃了sumb这个保存b数组前缀和的数组,ok吗?我们得到的公式是根据一个常量数组sum和一个不断被修改的b数组来求ans的。(注:这里ans为a数组的最终前缀和)
如果上面的公式没有问题,接下来我们的目标就是求b数组的二重和式。 来变形:
∑ j = 1 i ∑ k = 1 j b [ k ] = ∑ j = 1 i ( i − j + 1 ) ∗ b [ j ] = ( i + 1 ) ∑ j = 1 i b [ j ] − ∑ j = 1 i j ∗ b [ j ] \sum_{j = 1}^i \sum_{k = 1}^j b[k] = \sum_{j = 1}^i (i - j + 1)* b[j] = (i +1)\sum_{j = 1}^ib[j] - \sum_{j = 1}^i j* b[j] j=1∑ik=1∑jb[k]=j=1∑i(i−j+1)∗b[j]=(i+1)j=1∑ib[j]−j=1∑ij∗b[j]
首先,这个公式变换是常用套路,如果有问题可以自己用矩阵求和来推一下。其次,变换成这个公式的好处是我们只需维护 j * b[j] 的前缀和就可以直接求出a数组修改后的前缀和了。
具体操作是,我们建立两个树状数组c0和c1。其中c0是用来求b[j]前缀和的,而c1是用来求j * b[j]前缀和的。那么对于每个区间修改(为[l,r]上所有数字+val),我们执行4个操作:
另外,sum数组存放序列a的原始前缀和。所以每次查询[l,r]前缀和,当然还是拆分成求[1,r]和[1,l-1]的前缀和,二者相减。写成式子如下:
( s u m [ r ] + ( r + 1 ) ∗ a s k ( c 0 , r ) − a s k ( c 1 , r ) ) − ( s u m [ l − 1 ] + l ∗ a s k ( c 1 , l − 1 ) − a s k ( c 1 , l − 1 ) ) (sum[r] + (r+1) * ask(c0,r) - ask(c1,r) ) - (sum[l-1] + l * ask(c1,l-1) - ask(c1,l-1) ) (sum[r]+(r+1)∗ask(c0,r)−ask(c1,r))−(sum[l−1]+l∗ask(c1,l−1)−ask(c1,l−1))
这里以POJ3468为例:
#include
const int MAXN = 1e5+10;
typedef long long ll;
ll c[2][MAXN];
ll a[MAXN],sum[MAXN];
int n,m;
void add(int k,int x,int y){
for(;x <= n;x += x&-x) c[k][x] += y;
}
ll ask(int k,int x){
ll ans = 0;
for(;x;x -= x&-x) ans += c[k][x];
return ans;
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%lld",a+i);
for(int i = 1;i <= n;i++) sum[i] += sum[i-1] + a[i];
while(m--){
char op[10];
int l,r,val;
scanf("%s",op);
if(op[0] == 'C'){
scanf("%d%d%d",&l,&r,&val);
add(0,l,val);
add(0,r+1,-val);
add(1,l,l*val);
add(1,r+1,-(r+1)*val);
} else{
scanf("%d%d",&l,&r);
ll ans = sum[r] + (r+1)*ask(0,r) - ask(1,r);
ans -= sum[l-1] + l*ask(0,l-1) - ask(1,l-1);
printf("%lld\n",ans);
}
}
return 0;
}
参考书目: