树状数组 (三种模型)

感谢大佬的讲解:https://blog.csdn.net/sinat_37668729/article/details/77387668                        

 

1.改变某一元素的值,查询某一区间内所有元素的和(单点更新,区间查询)。

2.把一个区间内的所有元素都加上一个值,查询某一个元素的值(区间更新,单点查询)。

3.把某一个区间内的所有元素都加上一个值,查询某一区间内所有元素的和(区间更新,区间查询)。

第三种操作是转载RoBa大牛的,具体还得深入理解,现在暂当模板。虽然前面说是“区间更新”,或者“区间查询”,但实际上并不能做到像线段树那样完美,有着很大的局限性。

模式1:

#define Lowbit(p) (p&(-(p)))
//1.改变某一元素的值,查询某一区间内所有元素的和(单点更新,区间查询)。
//向上更新,向下求和
void Update(int *BIT,int p,int val)
{
    while ( p<=N )
    {
        BIT[p] += val;
        p += Lowbit(p);
    }
}

int GetSum(int *BIT,int p)
{
    int ret=0;
    while ( p>0 )
    {
        ret += BIT[p];
        p -= Lowbit(p);
    }
    return ret;
}

//向下更新,向上求和
void Update(int *BIT,int p,int val)
{
    while ( p>0 )
    {
        BIT[p] += val;
        p -= Lowbit(p);
    }
}

int GetSum(int *BIT,int p)
{
    int ret=0;
    while ( p<=N )
    {
        ret += BIT[p];
        p += Lowbit(p);
    }
    return ret;
}


模式2:  

把一个区间内的所有元素都加上一个值,查询某一个元素的值(区间更新,单点查询)。描述给定一个初始值都为0的序列,动态地修改一段连续位置上的数字,加上一个数,减去一个数,然后动态地提出问题,问题的形式是求出一个位置的数字。
输入
输入数据第一行包含2个整数N,M(1≤N,M≤100000),表示序列的长度和操作的次数。
接下来M行,每行会出现如下的一种格式:
* Add i j x ——将序列中第i个数到第j个数加上x
* Query i ——求出序列中第i个数
输出
对于每一个Query操作输出一个数,表示序列中第i个数。
分析:
我们把支持这种操作的树状数组称为树状数组的模式二,在这种模式下,a[i]已经不再表示真实的值了,只不过是一个没有意义的、用来辅助的数组。这时我们真正需要的是另一个假想的数组b[],b[i] 才表示真实的元素值。此时Sum(i)虽然也是求a[i]之前的元素和,但它现在表示的是实际我要的值,也就是 b[i]

注意,这里只是对a[i]进行了加一,i+1到j的值是没变的,所以求得和即为某点的值)

比如现在我要对图1 中a[]数组中红色区域的值全部1。当然你可以用模式一的 Update(i)对该区间内的每一个元素都修改一次,但如果这个区间很大,那么每次修改的复杂度就都是O(NlogN),m次修改就是O(MNlbN),这在M和N很大的时候仍是不满足要求的。这时模式二便派上了用场。我只要将该区域的第一个元素+1,最后一个元素的下一位置-1,对每个位置GetSum(i)以后的值见下图:

 


相信大家已经看得很清楚了,数组b[]正是我们想要的结果。模式二难理解主要在于 a[]数组的意义。这时请不要再管a[i]表示什么,a[i]已经没有意义了,我们需要的是b[i]!但模式二同样存在一个缺陷,如果要对某个区间内的元素求和,复杂度就变成O(NlogN)了。所以要分清两种模式的优缺点,根据题目的条件选择合适的模式,灵活应变!

 

//[l,r]中每个元素加上val
void _Update(int *BIT,int l,int r,int val)
{
    //调用的是模式一中的向上更新
    Update(BIT,l,val);
    Update(BIT,r+1,-val);
}

//询问p位置的数
int Querry(int BIT,int p)
{
    //调用的是模式一中的向下求和
    return GetSum(BIT,p);
}

 

模式3:

把某一个区间内的所有元素都加上一个值,查询某一区间内所有元素的和(区间更新,区间查询)。
简单的树状数组模型是不支持这样一组操作的:(1)把某一个区间内所有元素都加上一个值 (2)查询某一个区间内所有元素的和。当然,这个东西可以用线段树完成,但是线段树占内存比较大,写起来也比较繁(对我这种不会数据结构的人而言)。下面我们用一个改进版的树状数组完成这个任务。
首先一个观察是区间操作总可以变成从最左端开始,比如把区间[3..6]都加10,可以变成[1..6]加10, [1..2]减10。查询也类似。于是下面只关心从最左端开始的情况。定义Insert(p, d)表示把区间[1..p]都加d,Query(p)表示查询区间[1..p]之和。
我们考虑调用一次Insert(p, d)对以后的某次查询Query(q)的影响:
(1) 如果p<=q,总的结果会加上p*d (2) 如果p>q,总的结果会加上q*d
也就是说,Query(q)的结果来源可分为两部分,一部分是Insert(p1,d) (p1<=q),一部分是Insert(p2,d) (p2 > q)。我们用两个数组B[], C[]分别维护这两部分信息,B[i]表示区间右端点恰好是i的所有区间的影响之和,C[i]表示区间右端点大于i的所有区间的影响之和。每当遇到 Insert时,考虑当前的Insert会对以后的Query产生什么影响,更新B和C数组;当遇到Query时,把两部分的结果累加起来。
具体来说,当我们遇到Insert(p, d)时,把B[p]增加p*d,把C[1], C[2], …, C[p-1]都增加d。当遇到Query(p)时,查询B[1]+B[2]+…+B[p]+C[p]*p即可。可以发现对B数组是修改单个元素,查询区间和;对C数组是修改区间,查询单个元素,这恰好对应于一开始说的树状数组支持的基本操作。于是我们用两个树状数组漂亮地完成了任务。
 

#include <cstdio>
const int MAXN = 1024;
int B[MAXN], C[MAXN];
#define LOWBIT(x) ((x)&(-(x)))
void bit_update(int *a, int p, int d)
{
    for ( ; p && p < MAXN ; p += LOWBIT(p))
        a[p] += d;
}

int bit_query(int *a, int p)
{
    int s = 0;
    for ( ; p ; p -= LOWBIT(p))
        s += a[p];
    return s;
}

void bit_update2(int *a, int p, int d)
{
    for ( ; p ; p -= LOWBIT(p))
        a[p] += d;
}

int bit_query2(int *a, int p)
{
    int s = 0;
    for ( ; p && p < MAXN ; p += LOWBIT(p))
        s += a[p];
    return s;
}

inline void _insert(int p, int d)
{
    bit_update(B, p, p*d);
    bit_update2(C, p-1, d);
}

inline int _query(int p)
{
    return bit_query(B, p) + bit_query2(C, p) * p;
}

inline void insert_seg(int a, int b, int d)
{
    _insert(a-1, -d);
    _insert(b, d);
}

inline int query_seg(int a, int b)
{
    return _query(b) - _query(a-1);
}

int main()
{
    int com, a, b, c;
    while (scanf("%d%d%d",&com,&a,&b) != EOF)
    {
        a += 2;
        b += 2; //防止出现负数
        if (com == 0)   //更新
        {
            scanf("%d",&c);
            insert_seg(a, b, c);
        }
        else     //查询
        {
            printf("%d\n",query_seg(a,b));
        }
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值