树状数组-线段树

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;  
}  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值