树状数组
- 单点修改、区间查询
- 区间修改、单点查询
- 区间修改、区间查询
单点修改、区间查询
这里讲解树状数组的最基本操作单点修改、区间查询
,当然能做到单点修改、区间查询
,肯定就能做到单点修改、单点查询
了。树状数组是用来快速求前缀和的,传统的单点修改、区间查询要么单点修改的复杂度是
O
(
n
)
O(n)
O(n)查询是
O
(
1
)
O(1)
O(1),要么查询复杂度是
O
(
n
)
O(n)
O(n)修改是
O
(
1
)
O(1)
O(1),树状数组相当于一个比较综合的算法,树状数组的查询时间复杂度和修改时间复杂度都是
O
(
l
o
g
n
)
O(log n)
O(logn)的。
下面这张图基本上是讲解树状数组必用的一张图
树状数组初学起来还是比较难以理解,过弄明白之后代码就很好写了,树状数组相当于是拿空间换时间,原数组为a[]
,额外数组为c[]
,先来讲一下lowbit
操作
lowbit(x)返回x的最后一个1及其后面的0(可能不存在)构成的数字
比如
lowbit(3)=1, 3的二进制是11 ,最后一位1及其后面的0是1
lowbit(4)=4, 4的二进制是100 ,最后一位1及其后面的0是100
lowbit(6)=2, 4的二进制是110 ,最后一位1及其后面的0是10
lowbit函数也很好实现
lowbit(x)=x&-x
因为计算机是存的补码,-x相当于x取反再加1,例如x=10010,-x=1110
-x&x=10
树状数组主要有两个操作:
1.某一位置的数+x
2.求区间[1,r]的和([l,r]=[1,r]-[1,l-1])
观察上图可以发现
c[1]=a[1]
c[2]=c[1]+a[2]
c[3]=a[3]
c[4]=c[2]+c[3]+a[4]
...
c[16]=c[8]+c[12]+c[14]+c[15]+a[16]
假设我们已经维护好了c[]
数组,那么我们如何求前缀和呢
比如求a[1~15]
的和
我们可以发现
15的二进制为1111=1000+100+10+1=8+4+2+1
sum[1,15]=c[15]+c[14]+c[12]+c[8]=c[15-0]+c[15-0-1]+c[15-0-1-2]+c[15-0-1-2]+c[15-0-1-2-4]
有没有发现有什么关系
其中
lowbit(1111)=1,1111-1=1110
lowbit(1110)=2,1110-2=1100
lowbit(1100)=4,1100-4=1000
lowbit(1000)=8,1000-8=0
所以根据上面的推导就能写出查询函数query(r)
,查询[1,x]的前缀和
int query(int r){
int res=0;
for(int i=r;i>=0;i-=lowbit(i)) res+=f[i];
return res;
}
这是查询操作,我们还需要修改操作,每次修改a[]
(将a[i]变成b,就相当于a[i]+b-a[i],更改操作可以转换成+操作)都需要更改其对应的c[]
数组的值
a[2]对应的c[]数组有
c[2]、c[4]、c[8]、c[16],对应的二进制下标为
10 100 1000 10000
其中10=2
4=100=2+lowbit(2)
8=1000=4+lowbit(4)
16=10000=8+lowbit(8)
所以某一位置的数+x 的函数为
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i)) f[i]+=c;//这里f数组是上图中的c数组,c是要加的值x
}
至于为什么操作可以看b站这两个视频 五分钟丝滑动画讲解 | 树状数组、 〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组
有一道树状数组的最经典例题可以做一下,AcWing1264. 动态求连续区间和
代码如下:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int f[N];
int n,m;
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i)) f[i]+=c;
}
int query(int r){
int res=0;
for(int i=r;i>=1;i-=lowbit(i)) res+=f[i];
return res;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
add(i,x);
}
while(m--){
int k,l,r;
scanf("%d%d%d",&k,&l,&r);
if(k==0) printf("%d\n",query(r)-query(l-1));
else add(l,r);
}
return 0;
}
还有两道例题:
AcWing1265. 数星星
AcWing 241. 楼兰图腾
区间修改、单点查询
之前的单点修改区间查询,维护的是原数组的树状数组,这里我们维护差分数组的树状数组即可,可看例题 AcWing 242. 一个简单的整数问题
代码如下
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int n,m;
int c[N];
int lowbit(int x){
return x&-x;
}
void add(int idx,int x){
for(int i=idx;i<=n;i+=lowbit(i)) c[i]+=x;
}
int query(int r){
int res=0;
for(int i=r;i;i-=lowbit(i)) res+=c[i];
return res;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
add(i,x);
add(i+1,-x);
}
while(m--){
char op[2];
cin>>op;
if(*op=='Q'){
int r;
cin>>r;
cout<<query(r)<<endl;
}
else{
int l,r,d;
cin>>l>>r>>d;
add(l,d);
add(r+1,-d);
}
}
return 0;
}
区间修改、区间查询
区间修改,区间查询
较为复杂一些,要维护两个树状数组
首先有差分数组b[]
,原数组f[]
首先初始化有
b[1]=f[1]
b[2]=f[2]-f[1]
b[3]=f[3]-f[2]
......
b[n]=f[n]-f[n-1]
我们要区间修改,区间查询,区间修改可以通过差分来实现,区间查询的推导如下
要求区间[1,x]的和,就是求
∑
i
=
1
x
f
[
i
]
\sum\limits_{i=1}^{x}f[i]
i=1∑xf[i] ,其中
f
[
i
]
=
∑
j
=
1
i
b
[
j
]
f[i]=\sum\limits_{j=1}^{i}b[j]
f[i]=j=1∑ib[j],
∑
i
=
1
x
f
[
i
]
=
∑
i
=
1
x
∑
j
=
1
i
b
[
j
]
\sum\limits_{i=1}^{x}f[i]=\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}b[j]
i=1∑xf[i]=i=1∑xj=1∑ib[j]为了方便看,我们可以写成下面这种形式
f[1]= b[1]
f[2]= b[1]+b[2]
f[3]= b[1]+b[2]+b[3]
.....
f[x]= b[1]+b[2]+b[3]+.....+b[x]
右侧的这部分像矩阵,我们把这个矩阵补全
\b[1]+b[2]+b[3]+b[4]+.....+b[x]
b[1]\+b[2]+b[3]+b[4]+.....+b[x]
b[1]+b[2]\+b[3]+b[4]+.....+b[x]
b[1]+b[2]+b[3]\b[4]+.....+b[x]
..... \+b[x]
b[1]+b[2]+b[3]+..... +b[x]
这个矩阵的值为
(
x
+
1
)
∗
(
b
[
1
]
+
b
[
2
]
+
.
.
.
.
+
b
[
x
]
)
(x+1)*(b[1]+b[2]+....+b[x])
(x+1)∗(b[1]+b[2]+....+b[x]),我们补全这个矩阵用到的数的和为
1
∗
b
[
1
]
+
2
∗
b
[
2
]
+
.
.
.
+
x
∗
b
[
x
]
1*b[1]+2*b[2]+...+x*b[x]
1∗b[1]+2∗b[2]+...+x∗b[x]
所以
∑
i
=
1
x
f
[
i
]
=
∑
i
=
1
x
∑
j
=
1
i
b
[
j
]
\sum\limits_{i=1}^{x}f[i]=\sum\limits_{i=1}^{x}\sum\limits_{j=1}^{i}b[j]
i=1∑xf[i]=i=1∑xj=1∑ib[j]=
(
x
+
1
)
∗
(
b
[
1
]
+
b
[
2
]
+
.
.
.
.
+
b
[
x
]
)
−
(
1
∗
b
[
1
]
+
2
∗
b
[
2
]
+
.
.
.
+
x
∗
b
[
x
]
)
(x+1)*(b[1]+b[2]+....+b[x])-(1*b[1]+2*b[2]+...+x*b[x])
(x+1)∗(b[1]+b[2]+....+b[x])−(1∗b[1]+2∗b[2]+...+x∗b[x])
我们需要两个树状数组c1[]、c2[]
,c1是b数组的树状数组,c2是i*b[i]的树状数组
树状数组主要有两个操作,查询操作,这两个数组的操作是一样的,对于修改操作,例如在[l,r]
区间内加上一个数h
对于c1数组的修改操作为
add(c1,l,h)
add(c1,r+1,-h)
对于c2数组的修改操作为
add(c2,l,l*h)
add(c2,r+1,-(r+1)*h)
例题: AcWing 243. 一个简单的整数问题2
代码如下
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
typedef long long ll;
using namespace std;
const int N=1e5+10;
ll c1[N],c2[N];
int f[N];
int n,m;
int lowbit(int x){
return x&-x;
}
void add(ll c[],int idx,ll x){
for(int i=idx;i<=n;i+=lowbit(i)) c[i]+=x;
}
ll query(ll c[],int r){
ll res=0;
for(int i=r;i;i-=lowbit(i)) res+=c[i];
return res;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&f[i]);
for(int i=1;i<=n;i++){
add(c1,i,f[i]-f[i-1]);
add(c2,i,(ll)(f[i]-f[i-1])*i);
}
while(m--){
char op[2];
int l,r,d;
cin>>op;
if(*op=='C'){
cin>>l>>r>>d;
add(c1,l,d);
add(c1,r+1,-d);
add(c2,l,(ll)l*d);
add(c2,r+1,-(r+1)*(ll)d);
}
else{
cin>>l>>r;
cout<<(ll)(r+1)*query(c1,r)-(ll)query(c2,r)-(ll)(l)*query(c1,l-1)+(ll)query(c2,l-1)<<endl;
}
}
}