树状数组自学笔记
引入
考虑如下问题:
对于一个
n
n
n个元素的数组,我们需要对其进行
m
m
m次操作,操作有两种类型:
①将某数加上
k
k
k
②求出某个区间
[
l
,
r
]
[l,r]
[l,r]上内所有数字的和
数据范围: 1 < = n , k < = 5 × 1 0 5 1<=n,k<=5×10^5 1<=n,k<=5×105
考虑这题的做法:
①最朴素的暴力
对于操作1 直接修改元素 复杂度
O
(
1
)
O(1)
O(1)
对于操作2 直接遍历所查区间 复杂度
O
(
n
)
O(n)
O(n)
显然,若查询的次数较多会导致TLE
那为了实现
O
(
1
)
O(1)
O(1)查询,我们自然而然想到了前缀和
②用前缀和
对于操作1 输出
s
u
m
[
r
]
−
s
u
m
[
l
−
1
]
sum[r]-sum[l-1]
sum[r]−sum[l−1] 复杂度
O
(
1
)
O(1)
O(1)
然而对于操作2,某个元素的修改会对
s
u
m
sum
sum数组的值造成影响,在修改了某个元素后,我们需把
s
u
m
sum
sum数组中所修改元素后面的值也进行修改,故复杂度
O
(
n
)
O(n)
O(n)
由此,我们发现了,这两种做法都有局限性,所以我们需要学习一种整体复杂度低的做法,也就是今天的主角——树状数组
树状数组
维基百科
简单来说:最朴素的树状数组支持单点修改(修改某一元素的值)和区间查询(查询某一区间的和),二者的时间复杂度都为
O
(
l
o
g
n
)
O(logn)
O(logn),当然此外还有很多拓展。
树状数组,当然不是建树,而是用数组来模拟树形结构。
设数组
a
a
a为原数组
对于数组
c
c
c我们可以写出如下的式子
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]
.
.
.
.
.
.
......
......
可以看到,我们利用数组模拟出了树形的结构,这便是树状数组
lowbit函数
在讲解树状数组如何实现它的功能之前,我们先了解一个函数: l o w b i t ( x ) lowbit(x) lowbit(x)
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x)直译为中文意思是:低位。
我们定义它表示二进制表达式最低位的1所表示的数
比如:
l
o
w
b
i
t
(
10100
)
=
(
100
)
2
=
4
lowbit(10100)=(100)_2=4
lowbit(10100)=(100)2=4
l
o
w
b
i
t
(
101
)
=
1
lowbit(101)=1
lowbit(101)=1
那么对于一个数的
l
o
w
b
i
t
lowbit
lowbit,我们应该怎么求出呢?
根据小学二年级 C语言课上所学,计算机通常以二进制补码的形式存储整数,正数的补码表示是其本身,负数的补码表示是该数取反加一。
然后我们发现,对于一个整数x,它和它的相反数的补码总是满足这样的性质:存在某一位,它和它的相反数的这一位都为1,它和它的相反数在这一位的低位的值均为0,而高位每一位都相反。
例如:
(
+
101000
)
补
=
0101000
(+101000)_补=0101000
(+101000)补=0101000
(
−
101000
)
补
=
1011000
(-101000)_补=1011000
(−101000)补=1011000
而
(
+
101000
)
&
(
−
101000
)
=
(
0001000
)
(+101000)\&(-101000)=(0001000)
(+101000)&(−101000)=(0001000)
可以得出: l o w b i t ( x ) = x & − x lowbit(x)=x\&-x lowbit(x)=x&−x
l o w b i t lowbit lowbit函数如下:
int lowbit(int x)
{
return x&(-x);
}
区间查询
首先考虑树状数组如何实现区间查询
在此图中,若需要查询前7个元素的和,即
s
u
m
[
7
]
=
a
[
1
]
+
a
[
2
]
+
.
.
.
+
a
[
7
]
sum[7]=a[1]+a[2]+...+a[7]
sum[7]=a[1]+a[2]+...+a[7],
事实上,我们可以写成
s
u
m
[
7
]
=
(
a
[
1
]
+
a
[
2
]
+
a
[
3
]
+
a
[
4
]
)
+
(
a
[
5
]
+
a
[
6
]
)
+
(
a
[
7
]
)
sum[7]=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])+(a[7])
sum[7]=(a[1]+a[2]+a[3]+a[4])+(a[5]+a[6])+(a[7]),即
s
u
m
[
7
]
=
(
c
[
4
]
+
c
[
6
]
+
c
[
7
]
)
sum[7]=(c[4]+c[6]+c[7])
sum[7]=(c[4]+c[6]+c[7]),
在十进制下,我们可能发现不了什么规律,我们用二进制视角来看一下:
s
u
m
[
(
111
)
2
]
=
c
[
(
111
)
2
]
+
c
[
(
110
)
2
]
+
c
[
(
100
)
2
]
sum[(111)_2]=c[(111)_2]+c[(110)_2]+c[(100)_2]
sum[(111)2]=c[(111)2]+c[(110)2]+c[(100)2]
同样的我们可以写出:
s
u
m
[
(
110
)
2
]
=
c
[
(
110
)
2
]
+
c
[
(
100
)
2
]
sum[(110)_2]=c[(110)_2]+c[(100)_2]
sum[(110)2]=c[(110)2]+c[(100)2]
s
u
m
[
(
101
)
2
]
=
c
[
(
101
)
2
]
+
c
[
(
100
)
2
]
sum[(101)_2]=c[(101)_2]+c[(100)_2]
sum[(101)2]=c[(101)2]+c[(100)2]
s
u
m
[
(
1000
)
2
]
=
c
[
(
1000
)
2
]
sum[(1000)_2]=c[(1000)_2]
sum[(1000)2]=c[(1000)2]
显然 我们可以看出,求
s
u
m
[
i
]
sum[i]
sum[i]的过程我们可以看作,每次加上当前数
c
[
i
]
c[i]
c[i],并让
i
i
i去掉最低位的1,怎么实现去掉最低位的1?这就要用到刚刚所讲的
l
o
w
b
i
t
lowbit
lowbit函数了,而去掉最低位的1的操作,即为让i减去
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i)
区间查询的代码如下:
int query(int i)
{
int res=0;
while(i>0)
{
res+=c[i];
i-=lowbit(i);
}
return res;
}
这样我们可以求得区间
1
−
i
1-i
1−i的和,那对于区间
j
−
i
j-i
j−i的和
(
1
<
=
j
<
=
i
<
=
n
)
(1<=j<=i<=n)
(1<=j<=i<=n)
与前缀和的想法类似,只需要求出
q
u
e
r
y
(
i
)
−
q
u
e
r
y
(
j
−
1
)
query(i)-query(j-1)
query(i)−query(j−1)即可
单点修改
在树状数组修改某一个元素,虽不需要像前缀和思路一样,将后面的
s
u
m
[
i
]
sum[i]
sum[i]的值全部修改,但也不是只需要修改一个点就可以了,需要把该节点和其所祖先节点更改。
还是看刚刚的图
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]
假如我们需要更改
a
[
3
]
a[3]
a[3],那我们可以发现需要修改的是c[3],c[4],c[8]
假如我们需要更改
a
[
5
]
a[5]
a[5],那我们可以发现需要修改的是c[5],c[6],c[8]
假如我们需要更改
a
[
7
]
a[7]
a[7],那我们可以发现需要修改的是c[7],c[8]
还是一样的套路,我们写成二进制的形式来观察一下
a
[
(
11
)
2
]
对应
c
[
(
11
)
2
]
,
c
[
(
100
)
2
]
,
c
[
(
1000
)
2
]
a[(11)_2]对应c[(11)_2],c[(100)_2],c[(1000)_2]
a[(11)2]对应c[(11)2],c[(100)2],c[(1000)2]
a
[
(
101
)
2
]
对应
c
[
(
101
)
2
]
,
c
[
(
110
)
2
]
,
c
[
(
1000
)
2
]
a[(101)_2]对应c[(101)_2],c[(110)_2],c[(1000)_2]
a[(101)2]对应c[(101)2],c[(110)2],c[(1000)2]
a
[
(
111
)
2
]
对应
c
[
(
111
)
2
]
,
c
[
(
1000
)
2
]
a[(111)_2]对应c[(111)_2],c[(1000)_2]
a[(111)2]对应c[(111)2],c[(1000)2]
对于这三个
c
c
c数组的下标,我们分别 从小到大的看,可以发现,每次下标的增量都是对于当前的
i
i
i的一个
l
o
w
b
i
t
(
i
)
lowbit(i)
lowbit(i),直到增大到不超过
n
n
n的最大数
然后对于每个
c
[
i
]
c[i]
c[i]都加上
k
k
k,即可实现单点的修改
代码:
void update(int i,int k)
{
while(i<=n){
c[i]+=k;
i+=lowbit(i);
}
}
模板题
(不要忘记读入数据的时候就要用update函数来更新树状数组)
代码如下:
#include <bits/stdc++.h>
using namespace std;
int n,m;
int a[500100],c[500100];
int lowbit(int x)//
{
return x&(-x);
}
void update(int i,int k)
{
while(i<=n){
c[i]+=k;
i+=lowbit(i);
}
}
int query(int i)
{
int res=0;
while(i>0)
{
res+=c[i];
i-=lowbit(i);
}
return res;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
update(i,a[i]);
}
while(m--)
{
int op,x,y;
cin>>op>>x>>y;
if(op==1)
{
update(x,y);
}
else {
cout<<query(y)-query(x-1)<<endl;
}
}
return 0;
}
2.【模板】树状数组 2
此题让我们实现区间修改和单点查询,我们可以考虑读入数据时读入原数组的差分数组(
a
[
i
]
−
a
[
i
−
1
]
a[i]-a[i-1]
a[i]−a[i−1])
然后对于区间修改操作,我们只需要修改端点的元素,在
l
l
l处加上
k
k
k,在
r
+
1
r+1
r+1处减去
k
k
k,就实现了整个区间都加上了
k
k
k。而由于我们存储的是差分数组,所以我们在求和时求出的不是区间和了,而是单点处的值
代码如下:
#include <bits/stdc++.h>
using namespace std;
int n,m;
int a[500100],c[500100];
int lowbit(int x)
{
return x&(-x);
}
void update(int i,int k)
{
while(i<=n){
c[i]+=k;
i+=lowbit(i);
}
}
int getsum(int i)
{
int res=0;
while(i>0)
{
res+=c[i];
i-=lowbit(i);
}
return res;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
update(i,a[i]-a[i-1]);
}
while(m--)
{
int op,x,y;
cin>>op;
if(op==1)
{
int k;
cin>>x>>y>>k;
update(x,k);
update(y+1,-k);
}
else {
cin>>x;
cout<<getsum(x)<<endl;
}
}
return 0;
}
后记
树状数组有很多很多的应用,比如求逆序对等等,但由于本身能力有限,在这一篇博客中不再讲解,以后可能会有所补充