一,树状数组的优点
前缀和的思想,可以通过O(n)的预处理,使得多次查询区间值都是o(1),但只能解决不修改,多次查询的问题。
差分思想,能通过差分数组,将区间修改变成O(1)的,最后通过一次O(n),可以恢复成原来的数组,但只能解决多次修改一次询问。
那有没有一种方法,一边修改一边询问,且时间复杂度是可以接受的呢?
当然有,那就是树状数组,代码量小,且可以实现上述操作
二,了解树状数组前置知识点
熟悉位运算,略微涉及补码和补码相关知识
1),补码是计算机中存储整数的方法
第一位是符号位,0表示正数,1表示负数
正数的原码反码补码,三码相同,下面讲的是负数的情况。
反码就是对原码进行取反操作(符号位不变,其他位取反)
补码就对原码先反码,然后再加一
例如:-8 原码 :10001000 反码 11110111 补码 11111000
那如何通过上述知识来获取一个数字最后一位1呢?
我们以40为例,还是假设八位
Lowbit(40)=40&-40=00101000 & 11011000=00001000=8
&表示按位与,即全是1才为1
三,树状数组如何查询区间和
给定一个原数组A,下标 1~n ,
新建一个数组c ,让其中每一个数字,掌管一个区间和。
注:以下内容只需先理解是怎么运行,不用明白原理,因为后文会解释
c[x] 就掌管长度为lowbit(x)的范围,即掌管范围是[x-lowbit(x)+1,x]
这样要询问1~x的前缀和,可以先用lowbit(x)得到前缀里面的一段子区间和,然后x-lowbit(x)
重复操作得到其他区间和,因为只用算log次lowbit,所以log次lowbit便可以得到结果,所以求[L,R],只要算两个前缀和,再相减即可。
下面为代码
int lowbit(int x){
return x&(-x);
}
int query(int x){ //求1~x的区间和
int ans=0;
while(x)
{
ans+=d[x];
x-=lowbit(x);
}
return ans;
}
那么为什么可以这样呢?
首先先看一个树状数组的图
解释一下上图:A为原数组 C为树状数组(每一位存了他掌管的区间)即i-lowbit(i)+1到 i
C[1] lowbit(1)=1 范围 1-1
C[2] lowbit(2)=2 范围 1-2
C[3] lowbit(3)=1 范围 3-3
C[4] lowbit(4)=4 范围 1-4
C[5] lowbit(5)=1 范围 5-5
C[6] lowbit(6)=2 范围 5-6
C[7] lowbit(7)=1 范围 7-7
C[8] lowbit(8)=8 范围 1-8
而且有个重要性质当前节点位置i ,加上lowbit (i),即为它的父亲节点(可带入上图验证)
四,树状数组如何单点更新
若我们要让A i加上 4,那我们就让树状数组中全部包含A i的c i都加上 4
所以接下来就要求,找到包含Ai的所有区间 Ci
之前已经提过 lowbit 不光是掌控区间大小,还是当前点与父亲节点的距离
所以单点更新时,只要每次下标加lowbit ,且更新节点信息即可。
代码如下
void add(int x,int k){
while(x<=n)
{
d[x]+=k;
x+=lowbit(x);
}
}
完整代码如下
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int s[500050],d[500050];
int lowbit(int x){
return x&(-x);
}
void add(int x,int k){
while(x<=n)
{
d[x]+=k;
x+=lowbit(x);
}
}
int query(int x){ //求1~x的区间和
int ans=0;
while(x)
{
ans+=d[x];
x-=lowbit(x);
}
return ans;
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s[i];
}
for(int i=1;i<=n;i++)
{
add(i,s[i]);
}
for(int i=1;i<=m;i++)
{
int op,x,k;
cin>>op;
if(op==1)
{
cin>>x>>k;
add(x,k);
}
else{
cin>>x>>k;
int sum1=query(k);
int sum2=query(x-1);
cout<<sum1-sum2<<endl;
}
}
return 0;
}
五,树状数组的区间更新(还是推荐用线段树来维护区间更新)
树状数组是可以支持区间更新的,但是线段树在思维上更有优势,并且可拓展性强
A 还是原数组,D 表示差分数组,D[i]=A[i]-A[i-1],且让D1=A1
这样我们用树状数组Ci来维护Di这个差分数组,而不是Ai原数组
求Ai变成了D1 到Di的前缀和,用树状数组实现
如过要让 L-R区间加上x ,就变成了单点更新,即让D[L] 加x D[R+1] 减去x
ok下面是区间更新,单点查询完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int s[500050],d[500050];
int lowbit(int x){
return x&(-x);
}
void add(int x,int k){
while(x<=n){
d[x]+=k;
x+=lowbit(x);
}
}
int query(int x){ //求1~x的区间和
int ans=0;
while(x){
ans+=d[x];
x-=lowbit(x);
}
return ans;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
}
for(int i=1;i<=n;i++){
add(i,s[i]-s[i-1]);
}
for(int i=1;i<=m;i++){
int op;
cin>>op;
if(op==1)
{
int x,y,k;
cin>>x>>y>>k;
add(x,k);
add(y+1,-k);
}
else
{
int x;
cin>>x;
cout<<query(x)<<endl;
}
}
return 0;
}