本人第一次写博客,主要以我学习树状数组时的思维为时间线推进,如有不足请见谅
作用
树状数组是一种支持单点修改、区间查询的数据结构。码量很小,占用空间小,效率也较高,是入门维护区间和数据结构的不二之选
空间复杂度O(n) ;修改、查询复杂度O(log n)。
怎么做到
先看一道模板例题:https://www.luogu.com.cn/problem/P3374
显然,这里完全可以开一个普通数组,用最朴素的循环暴力求解,然后得到TLE声一片~ 10^5的数量级哪里是那么能好过的?那我们看看有什么可以优化的地方:大家也应该看出来,就比如当依次询问[1,3][1,4]区间的和时,会有一段重复计算的区间。当询问次数变多时,这种重复计算的劣势就体现的相当明显。
那么怎么办?这里就要介绍一种结构——树状数组啦!
问大家一个问题,若要计算[1,6]的区间和,你想到的办法是什么?绝对是最常规的方法,那如果我告诉你
与
的值呢?先不要问为什么是这两段区间和,我们很容易理解这二者相加就是要求的答案,而看到这,树状数组的基本原理便已可以知晓——我们将原始数组拆成不多于log n的区间,对于每一个树状数组元素c[x],满足:1.其管辖的区间右端就是x 2.区间向左“延伸”的长度为lowbit(x)。不知道lowbit是什么?不着急,听我细细道:
lowbit
将一个整数转化为2进制,你就获得了一个0/1串,从最低位往高位找,一直找到第1个1出现为止,然后打住!将你所找的从最低位到第一个1出现的这一段截取的0/1串所获得的数再转化为十进制,就是我们所谓的lowbit啦!举个例子,123的二进制是1111011,截取的0/1串为1,因此lowbit(123)=1,48的二进制是110000,截取的0/1串为10000,转化为10进制为16,因此lowbit(48)=16,实际计算,我们一般向下面代码一样算lowbit,至于为什么,位运算学过吗?学过的应该都明白,没学过的可以先去了解一下
int lowbit(int x){
return x & (-x);
}
一般地,树状数组元素c[x]管辖的区间就是[x-lowbit(x)+1,x],为什么规律那么奇奇怪怪的?这就牵扯到树状数组的一大妙处了!试试把c[1]一直到c[8]所代表的区间分别算出来?发现了什么?贴一张图感受一下c[x]数组相互的关系,可以连成如下的“父子关系图”(可以理解为一棵树),其中每个父节点的权值是它所有子节点的和,而每个c[x]的父节点就是c[x+lowbit(x)]
单点修改
假设一棵如上图所示的数已经建好,考虑怎样对一个元素的权值进行修改(最常见的是让其加上一个值)。实际上,如果改动的是a[x],我们要改动的只是管辖范围包含x的c,打个比方,改动a[5],那么对于上面的树状数组,我们修改的是这几个元素——c[5],c[6],c[8]
根据c[x]管辖区间的规律,a[x]一定在c[x]的管辖范围内,然后不断往父节点上跳,一直到顶(也就是到最大的c[n]),这样,我们就完成了对a[x](其实是树状数组)的修改,还是以改a[5]为例,它的改动顺序为: c[5]->c[6]->c[8]
void update(int x,int y){//树状数组中x位置 加上y
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=y;
}
}
建树
为什么树状数组的建树不放第一个讲,是因为我认为它的建树比较奇葩:常规建树方法就是将每个a[i]的值都单点加入树状数组,这样建树是靠单点修改一个个改过去的,我还是第一次见
区间查询
我们注意到一个事实:,为什么要提到这点?因为树状数组只能求区间[1,x]的和。那么查询[1,x]区间和到底怎么理解?首先,需要有c[x]的权值是肯定的,然后一直往左跳,跳到c[x]维护区间的左端点-1作为右端点这样一个区间的c[y](易知y=x-lowbit(x)),以x>0为循环条件,把跳到的位置所含的元素累加起来,最后得到的就是[1,x]的区间和啦
还是以查询[1,5]为例
代码按照思路打,也是十分清晰了
int sum(int x){//求区间a[1……x]的和
int sumn=0;
for(int i=x;i>0;i-=lowbit(i)) sumn+=c[i];
return sumn;
}
然后这道题就基本上捋顺了,贴完整代码
#include<bits/stdc++.h>
using namespace std;
int c[500005],n;
int lowbit(int x){
return x & (-x);
}
void update(int x,int y){//树状数组中x位置 加上y
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=y;
}
}
int sum(int x){//求区间a[1……x]的和
int sumn=0;
for(int i=x;i>0;i-=lowbit(i)) sumn+=c[i];
return sumn;
}
int main()
{
int m,a;cin>>n>>m;
int start,end,k;
for(int i=1;i<=n;i++){//建树
scanf("%d",&a);
update(i,a);
}
for(int i=1;i<=m;i++){
scanf("%d%d%d",&k,&start,&end);
if(k==2) printf("%d\n",sum(end)-sum(start-1));
else update(start,end);
}
return 0;
}
几个注意事项:
1.数组老问题,若要维护的元素有n个,c数组至少要开n+1个,因为树状数组是从下标1存的
2.注意[x,y]的区间和是[1,y]的区间和减去[1,x-1]的区间和,不是减[1,x]的
3.树状数组仅能解决2类问题:区间查询+单点修改 或者 区间修改+单点查询(用差分处理即可),且要满足结合律并可差分(如+,*,xor),其它问题用它效率感人或根本做不了。那么既有区间查询又有区间修改,该怎么做?线段树是也!