POJ 3468
操作:区间价值,区间求和
poj 3468 树状数组解法
一 算法
树状数组天生用来动态维护数组前缀和,其特点是每次更新一个元素的值,查询只能查数组的前缀和,
但这个题目求的是某一区间的数组和,而且要支持批量更新某一区间内元素的值,怎么办呢?实际上,
还是可以把问题转化为求数组的前缀和。
首先,看更新操作update(s, t, d)把区间A[s]...A[t]都增加d,我们引入一个数组delta[i],表示
A[i]…A[n]的共同增量,n是数组的大小。那么update操作可以转化为:
1)令delta[s] = delta[s] + d,表示将A[s]…A[n]同时增加d,但这样A[t+1]…A[n]就多加了d,所以
2)再令delta[t+1] = delta[t+1] - d,表示将A[t+1]…A[n]同时减d
然后来看查询操作query(s, t),求A[s]...A[t]的区间和,转化为求前缀和,设sum[i] = A[1]+...+A[i],则
A[s]+...+A[t] = sum[t] - sum[s-1],
那么前缀和sum[x]又如何求呢?它由两部分组成,一是数组的原始和,二是该区间内的累计增量和, 把数组A的原始
值保存在数组org中,并且delta[i]对sum[x]的贡献值为delta[i]*(x+1-i),那么
sum[x] = org[1]+…+org[x] + delta[1]x + delta[2](x-1) + delta[3]*(x-2)+…+delta[x]*1
= org[1]+…+org[x] + segma(delta[i]*(x+1-i)) //有时候动动笔比看一天都有用!!
= segma(org[i]) + (x+1)*segma(delta[i]) - segma(delta[i]*i),1 <= i <= x
这其实就是三个数组org[i], delta[i]和delta[i]*i的前缀和,org[i]的前缀和保持不变,事先就可以求出来,delta[i]和
delta[i]*i的前缀和是不断变化的,可以用两个树状数组来维护。
树状数组的解法比朴素线段树快很多,如果把long long变量改成__int64,然后用C提交的话,可以达到1047ms,
排在22名,但很奇怪,如果用long long变量,用gcc提交的话就要慢很多。
二 代码
C代码 收藏代码
#include <stdio.h>
#define DEBUG
#ifdef DEBUG
#define debug(...) printf( __VA_ARGS__)
#else
#define debug(...)
#endif
#define N 100002
#define lowbit(i) ( i & (-i) )
/* 设delta[i]表示[i,n]的公共增量 */
long long c1[N]; /* 维护delta[i]的前缀和 */
long long c2[N]; /* 维护delta[i]*i的前缀和 */
long long sum[N];
int A[N];
int n;
long long query(long long *array, int i)
{
long long tmp;
tmp = 0;
while (i > 0) {
tmp += array[i];
i -= lowbit(i);
}
return tmp;
}
void update(long long *array, int i, long long d)
{
while (i <= n) {
array[i] += d;
i += lowbit(i);
}
}
int main()
{
int q, i, s, t, d;
long long ans;
char action;
scanf("%d %d", &n, &q);
for (i = 1; i <= n; i++) {
scanf("%d", A+i);
}
for (i = 1; i <= n; i++) {
sum[i] = sum[i-1] + A[i];
}
while (q--) {
getchar();
scanf("%c %d %d", &action, &s, &t);
if (action == 'Q') {
ans = sum[t] - sum[s-1];
ans += (t+1)*query(c1, t) - query(c2, t);
ans -= (s*query(c1, s-1) - query(c2, s-1));
printf("%lld\n", ans);
}
else {
scanf("%d", &d);
/* 把delta[i](s<=i<=t)加d,策略是
*先把[s,n]内的增量加d,再把[t+1,n]的增量减d
*/
update(c1, s, d);
update(c1, t+1, -d);
update(c2, s, d*s);
update(c2, t+1, -d*(t+1));
}
}
return 0;
}
事实上,还可以不通过求s和t的前缀和,而是直接求出[s,t]的区间和,这是因为:
sum[t] = segma(org[i]) + (x+1)*segma(delta[i]) - segma(delta[i]*i) 1 <= i <= t
sum[s-1] = segma(org[i]) + s*segma(delta[i]) - segma(delta[i]*i) 1 <= i <= s-1
[s,t]的区间和可以表示为:
sum[t]-sum[s-1] = org[s] + … + org[t] + (t+1)(delta[s] + … + delta[t]) + (t-s+1)(delta[1] + … + delta[s-1])
- (delta[s]*s + … + delta[t]*t)
= segma(org[i]) +(t+1)* segma(delta[i]) - segma(delta[i]*i) , s <= i <= t
+ (t-s+1)*segma(delta[i]), 1 <= i <= s-1
问题转化为求三个数组org, delta[i]和delta[i]*i的区间和,而线段树可以直接求出区间和,所以又得到了另外一种
解法:
C代码 收藏代码
#include <stdio.h>
//#define DEBUG
#ifdef DEBUG
#define debug(...) printf( __VA_ARGS__)
#else
#define debug(...)
#endif
#define N 100002
/* 设delta[i]表示[i,n]的公共增量 */
long long tree1[262144]; /* 维护delta[i]的前缀和 */
long long tree2[262144]; /* 维护delta[i]*i的前缀和 */
long long sum[N];
int A[N];
int n, M;
/* 查询[s,t]的区间和 */
long long query(long long *tree, int s, int t)
{
long long tmp;
tmp = 0;
for (s = s+M-1, t = t+M+1; (s^t) != 1; s >>= 1, t >>= 1) {
if (~s&1) {
tmp += tree[s^1];
}
if (t&1) {
tmp += tree[t^1];
}
}
return tmp;
}
/* 修改元素i的值 */
void update(long long *tree, int i, long long d)
{
for (i = (i+M); i > 0; i >>= 1) {
tree[i] += d;
}
}
int main()
{
int q, i, s, t, d;
long long ans;
char action;
scanf("%d %d", &n, &q);
for (i = 1; i <= n; i++) {
scanf("%d", A+i);
}
for (i = 1; i <= n; i++) {
sum[i] = sum[i-1] + A[i];
}
for (M = 1; M < (n+2); M <<= 1);
while (q--) {
getchar();
scanf("%c %d %d", &action, &s, &t);
if (action == 'Q') {
ans = sum[t] - sum[s-1];
ans += (t+1)*query(tree1, s, t)+(t-s+1)*query(tree1, 1, s-1);
ans -= query(tree2, s, t);
printf("%lld\n", ans);
}
else {
scanf("%d", &d);
/* 把delta[i](s<=i<=t)加d,策略是
*先把[s,n]内的增量加d,再把[t+1,n]的增量减d
*/
update(tree1, s, d);
update(tree2, s, d*s);
if (t < n) {
update(tree1, t+1, -d);
update(tree2, t+1, -d*(t+1));
}
}
}
return 0;
}
两种解法本质上是一样的,其实zkw式线段树 == 树状数组,它们都可以支持查询某个区间的和,以及修改某个点的值,
但不能直接修改某个区间的值,必须引入一个额外的数组,如这题的delta数组,把对区间的修改转化为对两个端点的修改。
线段树:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<queue>
#include<algorithm>
#include<map>
#include<iomanip>
#define INF 99999999
using namespace std;
const int MAX=100000+10;
__int64 sum[MAX<<2],mark[MAX<<2];//sum表示区间和,mark表示父节点更新了但是孩子未更新
void BuildTree(int n,__int64 left,__int64 right){
mark[n]=0;
if(left == right){scanf("%I64d",&sum[n]);return;}
__int64 mid=left+right>>1;
BuildTree(n<<1,left,mid);
BuildTree(n<<1|1,mid+1,right);
sum[n]=sum[n<<1]+sum[n<<1|1];
}
void Upchild(int n,__int64 len){
if(mark[n]){//表示该区间更新了但是孩子未更新
mark[n<<1]+=mark[n];//表示孩子更新了但是孩子的孩子未更新
mark[n<<1|1]+=mark[n];
sum[n<<1]+=(len-(len>>1))*mark[n];
sum[n<<1|1]+=(len>>1)*mark[n];
mark[n]=0;//表示不存在该区间更新了但是孩子未更新的情况
}
}
void Update(__int64 L,__int64 R,__int64 date,int n,__int64 left,__int64 right){
if(L<=left && right<=R){
sum[n]+=(right-left+1)*date;
mark[n]+=date;//表示父节点更新了但是孩子未更新
return;
}
Upchild(n,right-left+1);//在本次更新前先更新上一次父节点更新但是孩子未更新的孩子
__int64 mid=left+right>>1;
if(L<=mid)Update(L,R,date,n<<1,left,mid);
if(R>mid)Update(L,R,date,n<<1|1,mid+1,right);
sum[n]=sum[n<<1]+sum[n<<1|1];
}
__int64 Query(__int64 L,__int64 R,int n,__int64 left,__int64 right){
if(L<=left && right<=R)return sum[n];
Upchild(n,right-left+1);
__int64 mid=left+right>>1,ans=0;
if(L<=mid)ans+=Query(L,R,n<<1,left,mid);
if(R>mid)ans+=Query(L,R,n<<1|1,mid+1,right);
return ans;
}
int main(){
int m;
__int64 a,b,c,n;
char s[2];
while(scanf("%I64d%d",&n,&m)!=EOF){
BuildTree(1,1,n);
while(m--){
scanf("%s",s);
if(s[0] == 'C'){
scanf("%I64d%I64d%I64d",&a,&b,&c);
Update(a,b,c,1,1,n);
}
else{
scanf("%I64d%I64d",&a,&b);
printf("%I64d\n",Query(a,b,1,1,n));
}
}
}
return 0;
}