您的位置:首页 > 其它

树状数组-线段树

2016-03-15 13:01 316 查看
POJ 3468

操作:区间价值,区间求和

poj 3468 树状数组解法

一 算法

树状数组天生用来动态维护数组前缀和,其特点是每次更新一个元素的值,查询只能查数组的前缀和,

但这个题目求的是某一区间的数组和,而且要支持批量更新某一区间内元素的值,怎么办呢?实际上,

还是可以把问题转化为求数组的前缀和。

首先,看更新操作update(s, t, d)把区间A[s]...A[t]都增加d,我们引入一个数组delta[i],表示


A[i]…A
的共同增量,n是数组的大小。那么update操作可以转化为:

1)令delta[s] = delta[s] + d,表示将A[s]…A
同时增加d,但这样A[t+1]…A
就多加了d,所以

2)再令delta[t+1] = delta[t+1] - d,表示将A[t+1]…A
同时减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
;    /* 维护delta[i]的前缀和 */
long long c2
;    /* 维护delta[i]*i的前缀和 */
long long sum
;
int       A
;
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
;
int     A
;
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
=0;
if(left == right){scanf("%I64d",&sum
);return;}
__int64 mid=left+right>>1;
BuildTree(n<<1,left,mid);
BuildTree(n<<1|1,mid+1,right);
sum
=sum[n<<1]+sum[n<<1|1];
}

void Upchild(int n,__int64 len){
if(mark
){//表示该区间更新了但是孩子未更新
mark[n<<1]+=mark
;//表示孩子更新了但是孩子的孩子未更新
mark[n<<1|1]+=mark
;
sum[n<<1]+=(len-(len>>1))*mark
;
sum[n<<1|1]+=(len>>1)*mark
;
mark
=0;//表示不存在该区间更新了但是孩子未更新的情况
}
}

void Update(__int64 L,__int64 R,__int64 date,int n,__int64 left,__int64 right){
if(L<=left && right<=R){
sum
+=(right-left+1)*date;
mark
+=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
=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
;
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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: