一、简介
1 定义
树状数组是一种用于维护序列前缀和的数据结构,它支持单点修改和区间查询,时间复杂度均为 O(logn)。树状数组的核心思想是利用二进制的思想将序列分成若干个区间,从而实现快速查询和修改。
2 优点
假设我们有一个序列 a a a,我们需要支持两种操作:
1.单点修改:将
a
i
a_i
ai的值加上
x
x
x。
2.区间查询:查询
a
1
a_1
a1到
a
i
a_i
ai的和。
如果我们使用普通数组保存序列并实现上述两种操作,则时间复杂度分别为
O
(
n
)
O(n)
O(n)和
O
(
1
)
O(1)
O(1)如果使用前缀和,则时间复杂度分别为
O
(
1
)
O(1)
O(1)和
O
(
n
)
O(n)
O(n)。假设我们有
n
n
n个操作,则最坏情况下的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
那么,有没有一种可能使得时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),也就是两个操作的时间复杂度都是
O
(
l
o
g
n
)
O(logn)
O(logn)呢?树状数组便是支持上述两种操作,且时间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)的一种数据结构。
二、实现—单点修改+区间查询
假设输入时原数组为 a a a,树状数组为 c c c。
0 预备知识
在位运算中,有一种操作,可以只保留 i i i的二进制表示中最右边的 “ 1 ” “1” “1”: l o w b i t ( x ) = x lowbit(x)=x lowbit(x)=x& ( − x ) (-x) (−x)
int lowbit(int x){
return x&(-x);
}
1 结构
如图,我们可以发现:
c
[
1
]
=
a
[
1
]
c[1]=a[1]
c[1]=a[1]
c
[
2
]
=
a
[
1
]
+
a
[
2
]
c[2]=a[1]+a[2]
c[2]=a[1]+a[2]
c
[
3
]
=
a
[
3
]
c[3]=a[3]
c[3]=a[3]
c
[
4
]
=
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
c[4]=a[1]+a[2]+a[3]+a[4]
c[4]=a[1]+a[2]+a[3]+a[4]
c
[
5
]
=
a
[
5
]
c[5]=a[5]
c[5]=a[5]
c
[
6
]
=
a
[
5
]
+
a
[
6
]
c[6]=a[5]+a[6]
c[6]=a[5]+a[6]
c
[
7
]
=
a
[
7
]
c[7]=a[7]
c[7]=a[7]
c
[
8
]
=
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
+
a
[
5
]
+
a
[
6
]
+
a
[
7
]
+
a
[
8
]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
如果将它们转变成二进制,可以发现,每一层的
l
o
w
b
i
t
lowbit
lowbit都是相同的。
因此,节点
x
x
x的父节点为
x
+
l
o
w
b
i
t
(
x
)
x+lowbit(x)
x+lowbit(x)。
2 操作
2.2 单点更新
更新操作是将输入数组中的一个元素增加一个值。在树状数组中,这需要更新多个元素。具体来说,对于输入数组中位置
i
i
i的更新,我们需要更新所有包含区间i的树状数组元素。这些元素可以通过在
i
i
i上加
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i)得到:
c
[
i
]
=
a
[
i
−
2
k
+
1
]
+
a
[
i
−
2
k
+
2
]
+
…
…
a
[
i
]
c[i]=a[i-2^k+1]+a[i-2^k+2]+……a[i]
c[i]=a[i−2k+1]+a[i−2k+2]+……a[i]
void updata(int x,int y){
//x为更新位置,y为变化量
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=y;
}
}
2.3 区间查询
查询操作是计算输入数组中前 i i i个元素的和。在树状数组中,这需要累加多个元素。具体来说,我们从位置 i i i开始,每次去掉 l o w b i t ( i ) lowbit(i) lowbit(i),直到 i i i变为 0 0 0。
int find_sum(ll x){
int ans=0;
for(int i=x;i;i-=lowbit(i)){
ans+=c[i];
}
return ans;
}
注意,这个代码只能求区间 [ 1 , x ] [1,x] [1,x]的区间和,如果想求 [ l , r ] [l,r] [l,r]的区间和,利用前缀和相减就可以了—— [ l , r ] = [ 1 , r ] − [ 1 , l − 1 ] [l,r]=[1,r]-[1,l-1] [l,r]=[1,r]−[1,l−1]
3 实践
1 BIT-1
题面
给定数组
1
,
2...
a
1
,
a
2
.
.
.
a
n
1,2...a_1,a_2...a_n
1,2...a1,a2...an,进行
q
q
q次操作,操作有两种:
1
i
i
i
x
x
x:将
a
i
a_i
ai加上
x
x
x;
2
l
l
l
r
r
r:求
a
l
+
.
.
.
+
a
r
a_l +...+a_r
al+...+ar
输入描述
第一行:输入两个数
n
n
n,
q
q
q,表示给定数组的长度和操作数
第二行:输入
n
n
n个数,表示长度为
n
n
n的给定数组
接下来
q
q
q行:每行输入3个数字,表示如题的某一种操作
输出描述
对于每个2操作,输出对应结果
思路
板子题,直接写
AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll n,q,c[1000002];
ll lowbit(ll x){
return x&(-x);
}
void updata(ll x,ll y){
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=y;
}
}
ll find_sum(ll x){
ll ans=0;
for(ll i=x;i;i-=lowbit(i)){
ans+=c[i];
}
return ans;
}
int main(){
scanf("%lld%lld",&n,&q);
for(int i=1;i<=n;i++){
ll x;
scanf("%lld",&x);
updata(i,x);
}
while(q--){
ll k;
scanf("%lld",&k);
if(k==1){
ll i,x;
scanf("%lld%lld",&i,&x);
updata(i,x);
}
else{
ll l,r;
scanf("%lld%lld",&l,&r);
printf("%lld",find_sum(r)-find_sum(l-1));pr;
}
}
return 0;
}
2 逆序对
题面
对于给定的一段正整数序列,逆序对就是序列中
i
<
j
i<j
i<j且
a
i
>
a
j
a_i>a_j
ai>aj的有序对,求一序列中有多少个逆序对。
第一行:输入一个数
n
n
n,表示序列有多少个数字
第二行:输入
n
n
n个数字
a
i
a_i
ai
输出序列中逆序对的数目
思路
从后向前遍历
PS:数据太大要开离散化
AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
const ll N=1e6+5;
ll n,cnt[N],a[N],b[N];
ll sum;
map<ll,ll> mp;
ll lowbit(ll x){return x&(-x);}
void update(ll x,ll y){for(int i=x;i<=n;i+=lowbit(i)){cnt[i]+=y;}}
ll find(ll x){ll ans=0;for(int i=x;i;i-=lowbit(i)){ans+=cnt[i];}return ans;}
int main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
b[i]=a[i];
}
sort(a+1,a+1+n);
for(int i=1;i<=n;i++){
mp[a[i]]=i;
}
for(int i=1;i<=n;i++){
ll x=mp[b[i]];
update(x,1);
sum+=i-find(x);
}
printf("%lld",sum);
return 0;
}
三、实现—区间修改+单点查询
1 结构
树状数组虽然可以在 O ( l o g n ) O(logn) O(logn)的时间复杂度内完成单点更新或查询前缀和的操作。但是,如果我们想要进行区间修改和单点查询,我们需要使用到树状数组的一个变种——差分树状数组。
2 操作
差分树状数组的主要思想是将原序列转化为差分序列,然后在差分序列上建立树状数组。差分序列的第 i i i个元素等于原序列的第 i i i个元素和第 i − 1 i-1 i−1个元素的差。
2.0 前置—差分中的区间修改
当修改 a l a_l al~ a r a_r ar时(如 + k +k +k),需要将差分数组中第 l l l个位置 + k +k +k,同时将第 r + 1 r+1 r+1个位置 − k -k −k。
2.1 建立/区间修改
在输入时,不能以原数组进行建树,而是使用其差分数组。具体见下:
long long x,last=0;
for(int i=1;i<=n;i++){
scanf("%lld",&x);
update(i,x-last);
last=x;
}
当进行区间修改时,利用差分数组的特性即可。
long long l,r,k;
scanf("%lld%lld%lld",&l,&r,&k);
update(l,k);
update(r+1,-k);
3 实践
BIT-2
题面
给定数组
a
1
,
a
2
.
.
.
a
n
a_1 ,a_2 ...a_n
a1,a2...an ,进行q次操作,操作有两种:
1 l r k
:将
a
i
a_i
ai~
a
r
a_r
ar 每个数都加上 k;
2 k
:输出
a
k
a_k
ak
思路
和上面一样
AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll n,q,c[1000002];
ll lowbit(ll x){
return x&(-x);
}
void update(ll x,ll y){//单点修
//x为更新位置,y为变化量
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=y;
}
}
ll query(ll x){//区间查询
ll ans=0;
for(ll i=x;i;i-=lowbit(i)){
ans+=c[i];
}
return ans;
}
int main(){
scanf("%lld%lld",&n,&q);
ll x,last=0;
for(int i=1;i<=n;i++){
scanf("%lld",&x);
update(i,x-last);
last=x;
}
while(q--){
ll k;
scanf("%lld",&k);
if(k==1){
ll l,r,k;
scanf("%lld%lld%lld",&l,&r,&k);
update(l,k);
update(r+1,-k);
}
else{
ll x;
scanf("%lld",&x);
printf("%lld",query(x));pr;
}
}
return 0;
}
三、实现—区间修改+区间查询
1 操作
还是差分,我们只需要两个数组,分别存储运输组和差分数组。
2 实践
2.1 BIT-3
给定数组
a
1
,
a
2
.
.
.
a
n
a_1,a_2...a_n
a1,a2...an ,进行q次操作,操作有两种:
1 r k
:将
a
i
a
r
a_i ~ a_r
ai ar 每个数都加上k;
2 1 r
:求
a
1
+
.
.
.
+
a
r
a_1 + ... + a_r
a1+...+ar
AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll read(){ll x=0,t=1;char ch;ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-'){t=-1;}ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*t;}
ll n,q;
ll a,b,v;
ll k,now,last;
ll sum1,sum2;
ll c1[1000005];
ll c2[1000005];
ll lowbit(ll x){return x&(-x);}
void update(ll *t,ll x,ll w){
while(x<=n){
t[x]+=w;
x+=lowbit(x);
}
return ;
}
ll query(ll *t,ll x){
ll s=0;
while(x>0){
s+=t[x];
x-=lowbit(x);
}
return s;
}
int main(){
n=read();q=read();
for(ll i=1;i<=n;i++){
now = read();
update(c1,i,now-last);
update(c2,i,(i-1)*(now-last));
last=now;
}
while(q--){
k =read();
if(k==1){
a=read(),b=read(),v=read();
update(c1,a,v);update(c1,b+1,-v);
update(c2,a,v*(a-1));
update(c2,b+1,-v*b);
}
if(k==2){
a = read(),b = read();
sum1=(a-1)*query(c1,a-1)-query(c2,a-1);
sum2=b*query(c1,b)-query(c2,b);
printf("%lld\n",sum2-sum1);
}
}
return 0;
}
2.2 前缀和的前缀和
前缀和(prefix sum)
S
i
=
∑
k
=
1
i
a
k
S_i=\sum_{k=1}^i a_k
Si=∑k=1iak。
前前缀和(preprefix sum) 则把
S
i
S_i
Si作为原序列再进行前缀和。记再次求得前缀和第i个是
S
S
i
SS_i
SSi
给一个长度n的序列
a
1
,
a
2
,
⋯
,
a
n
a_1, a_2, \cdots, a_n
a1,a2,⋯,an,有两种操作:
Modify i x
:把 a i a_i ai改成 x x x;Query i
:查询 S S i SS_i SSi
思路
将差分数组改为前缀和数组即可。
AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll read(){ll x=0,t=1;char ch;ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-'){t=-1;}ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*t;}
ll n,m,last,t1[1000008],t2[1000008],a[1000008];
ll lowbit(ll x){return x&(-x);}
void update(ll pos,ll x){
for(ll i=pos;i<=n;i+=lowbit(i)){
t1[i]+=x,t2[i]+=pos*x;
}
}
ll query(ll pos){
ll res=0;
for(ll i=pos;i;i-=lowbit(i)){
res+=t1[i]*(pos+1)-t2[i];
}
return res;
}
char opt[8];
int main(){
n=read(),m=read();
for(ll i=1,x;i<=n;i++){
a[i]=read();
update(i,a[i]);
}
for(ll i=1,x,y;i<=m;i++){
scanf("%s",opt+1);
if(opt[1]=='Q'){
x=read();
printf("%lld",query(x));pr;
}
else{
x=read(),y=read();
update(x,y-a[x]);
a[x]=y;
}
}
}
四、吐槽~~
树状数组扩展性太小了,我爱线段树