树状数组基础模板

树状数组是可用于求区间和并支持单点修改的高效数据结构,
在讲树状数组时往往有这样一张图:
1605450-20190703084119814-905110056.png
看上去非常恶心,也很难以理解(稍加理解却不知如何实现)...
然而ta的代码非常简短,起码作为一道板子题,ta是非常图样图森破辣...
众所周知,求前缀和的朴素算法思路无比简单,其代码就像这样:

int sum[233];
int a[233];  //everything is '0'
...          //something like read
for(int i=1;i<=100;i++){
    sum[i]=sum[i-1]+a[i];
}

p.s.最近写中文注释爆炸了(乱码),然后改用英语小文章了
由以上代码知,这个朴素算法非常暴力,并不能支持修改操作,修改就TLE,
现在用树状数组这样一种数据结构,将特定的元素归纳到一定的集合中,这样一来单点修改是就可以只修改包含该元素的节点,效率高不少
具体是怎么实现的呢?
树状数组是基于"数的2进制的唯一分解性质"的数据结构,
上面那个性质就是每个数所表示的二进制数是不同的,正确性自己证明
基于这个性质,一个\(1\simx\)的区间可以表示成\(log_2 x\)个小区间
长度都可表示为\(2^n\),
举个例子(举蓝书上的):
\(x=(21)_{10}=(10101)_{2}\)
\(len=2^4->1\sim(2^4)\)
\(len=2^2->(2^4+1)\sim(2^4+2^2)\)
\(len=2^0->(2^4+2^2+1)\sim(2^4+2^2+2^0)\)
由此可见,每个区间长度为二进制分解下最低位的2次幂
那么可以考虑用其进行区间之间的转换,
先考虑如何求其值
现在要求最低位的2次幂及更低位0所组成的数,
现在考虑这样一个二进制数:
其某一位为1,更低位全部为0,
那么将其取反,0变成1,1变成0
那么这一位原来是最低的1变成了0,后面变成了1,
再给它的反码+1,使最后我们要求的几位与原数一模一样,此时取一个&,就可以取得其值
而数\(n\)的反码为\(n-1\),那么所要求的\(lowbit\)就是\(n\& -n\)
这就是\(lowbit\)的求法,
而上面图片中的c数组就是基于其上的树形结构,其中元素满足以下性质:
1.对于其内部节点,都表示的是以其为根叶节点的和,注意这里是叶节点,不是所有编号小于等于其角标的元素(不然就成前缀和了)
2.每个节点的子节点个数等于\(lowbit(**ta的角标**)\)其实就是区间长度
3.非根节点的父亲角标一定是\(x+lowbit(x)\)
那么有了父与子的关系,就方便多了嘛...
先不说每个函数及其实现,先按题的流程来走一遍当然读入输出就...
首先建树(状数组),将每个读入的点加入,并对其祖宗进行维护,就是把ta家长都"家访"一遍,把该节点的值加给家长
读入完成时可以保证对应节点值的正确性,
在此声明一下
这里的家长节点只含其树状结构中的子节点,并不是ta之前的元素都含
也就是说,我们在区间求和时(基础树状数组好像只能求区间为\(1\sim n\)的前缀和)会将区间看成小区间,然后分别将其包含的节点加起来组成前缀和,不重复,不遗漏这TM分治???
建树完成之后,可以进行\(m\)次操作了,就是单点修改和区间查询,
对于单点修改,可以重复与建树一样的步骤
其实建树用的是单点修改的函数,因为二者互通,
对于建树,或称单点修改函数的实现及原理在上面已经给出证明与解析(其实我知道我是给了的,但是我混乱而混蛋地忘了写在哪里了...)
对于区间查询,就是对于任意左端点(\(l\))和右端点(\(r\)),先求出\(sum(r)\),再求出\(sum(l-1)\),相减即得答案,
区间查询函数的原理就是上面提到的树状数组"分治法"
(这好像就是单点修改正确性证明哦...)
即每次通过\(lowbit\)函数来将个个小区间联系起来(这就是\(for\)循环枚举时的循环变量变化操作),将遍历到的子节点全部相加,函数返回其总和值,这就是区间\(1\sim n\)的和,同样按照上述步骤求得左端点(当然要减去1,这样才是完整的需查询区间)为区间的总和...不再重复阐述步骤嘿!又水一行(滑稽)
这就是树状数组的理论依据(可能有表述不清),代码实现如下:

// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
inline int lowbit(const int &n){
    return (n)&(-n);
}
int n;
int a[500005],c[2000005];
inline void add(const int &x,const int &y){
    for(int i=x;i<=n;i+=lowbit(i))
        c[i]+=y;
}
inline int sum(const int &x){
    int ans=0;
    for(int i=x;i;i-=lowbit(i)){
        ans+=c[i];
    }
    return ans;
}
int m;
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        add(i,a[i]);
    }
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        if(a==1)
            add(b,c);
        else
            printf("%d\n",sum(c)-sum(b-1));
    }return 0;
}

依旧是这样令人喜爱的码风呢

转载于:https://www.cnblogs.com/648-233/p/11124408.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值