引入
我们来看下面的例题:(洛谷 P3374 【模板】树状数组 1)
已知一个数列,你需要进行下面两种操作:
1.将某一个数加上 x x x
2.求出某区间每一个数的和
我们有哪些方法呢?
- 暴力。单点修改的时间复杂度为 O ( 1 ) O(1) O(1),区间查询的复杂度为 O ( n ) O(n) O(n)。
- 前缀和。单点修改的时间复杂度为 O ( n ) O(n) O(n),区间查询的复杂度为 O ( 1 ) O(1) O(1)。
我们可以看到,两种做法都有一个操作是 O ( 1 ) O(1) O(1)的,但也都有一个操作是 O ( n ) O(n) O(n)的。
那有没有什么两全其美的做法呢?当然有了。有的人可能就会喊了:线段树!
线段树固然好,但它也有它的缺点:算法常数大,编写难度高。
那就轮到我们的主角登场了:树状数组!
树状数组
树状数组顾名思义,不是树,而是树状的数组。树状数组常数小,编写难度小,且能够解决线段树所能解决的部分问题。
是的,是部分问题。能用线段树解决的问题,不一定能用树状数组解决。而能用树状数组解决的问题,全都能被线段树解决。
但是,由于上述的优点,树状数组仍然是一种十分重要的数据结构。
我们来看一下树状数组的结构:
红色的格子代表树状数组,白色的长条是为了便于理解。
我们用数组
c
[
]
c[]
c[]来存储树状数组,
a
[
]
a[]
a[]来存储原始数据。
我们可以看到,每个序号都有它的红色格子。我们以序号
4
4
4为例,来分析一下:序号
4
4
4对应的红色格子以及左边的长条覆盖了下面
1
1
1 ~
4
4
4的位置,表示
c
[
4
]
c[4]
c[4]存储的是
a
[
1
]
a[1]
a[1] ~
a
[
4
]
a[4]
a[4]的和。
至于 c [ ? ] c[?] c[?],存储 a [ ? ] a[?] a[?]到 a [ ? ] a[?] a[?]的和,这是怎么算出来的,这是树状数组的关键,留到最后讲。
区间查询、单点修改
这样,我们就可以实现区间查询了。比如说,我们要求 [ 3 , 7 ] [3,7] [3,7]内每个数的和,利用前缀和的思想,可以用 [ 1 , 7 ] [1,7] [1,7]的和减去 [ 1 , 2 ] [1,2] [1,2]的和。 [ 1 , 2 ] [1,2] [1,2]的和,即为 c [ 2 ] c[2] c[2]的值, [ 1 , 7 ] [1,7] [1,7]的和,即为 c [ 4 ] + c [ 6 ] + c [ 7 ] c[4]+c[6]+c[7] c[4]+c[6]+c[7]。
至于单点修改,若我们要修改a[2]的值,因为只有 c [ 2 ] c[2] c[2], c [ 4 ] c[4] c[4], c [ 8 ] c[8] c[8]中有 a [ 2 ] a[2] a[2],所以我们只需要修改 c [ 2 ] c[2] c[2], c [ 4 ] c[4] c[4], c [ 8 ] c[8] c[8]的值就可以了。
这样,我们就做到了 O ( l o g 2 n ) O(log_2 n) O(log2n)的区间查询和单点修改。
lowbit的实现
所以,只剩下最重要的部分了,也就是 c [ ] c[] c[]存储和的问题。
注意,这个地方有点难理解,而且会用到二进制,做好思想准备。
这里,我们会用到一个叫 l o w b i t lowbit lowbit的东西。所谓 l o w b i t lowbit lowbit,就是“求二进制下最低位 1 1 1的运算”。
不明白?不要紧。让我们来模拟一下。以 6 6 6为例:
(
6
)
10
(6)_{10}
(6)10的二进制为
(
110
)
2
(110)_2
(110)2。
从右往左数,数到第一个
1
1
1。
第一个
1
1
1及其右边的部分为
10
10
10。
(
10
)
2
(10)_2
(10)2的十进制是
(
2
)
10
(2)_{10}
(2)10。
所以
c
[
6
]
c[6]
c[6]储存的就是包括
6
6
6向左数的
2
2
2个数,即
a
[
5
]
a[5]
a[5]和
a
[
6
]
a[6]
a[6]。
对照一下树状数组的结构图,是不是?
那 l o w b i t lowbit lowbit是怎样实现的呢?
int lowbit(int x)
{
return x&(-x);
}
就这么简单。那原理呢?
我们知道,计算机中的负数是用补码存储的,补码就是反码
+
1
+1
+1,一个正数转变为负数,就是原码转化为补码的过程。我们以
10010
10010
10010为例:
原码:
10010
10010
10010
反码:
01101
01101
01101(原码取反)
补码:
01110
01110
01110(反码
+
1
+1
+1,表示负数)
可以看到,补码的前一部分与原码相反,后一部分与原码相同,而相同的那一部分就是我们要求的
l
o
w
b
i
t
lowbit
lowbit。
这样,我们就解决了最后一个问题。
完整代码
#include<iostream>
#include<cstdio>
using namespace std;
int n,q;
int op,x,y;
int a[1000010];
int c[1000010];
int lowbit(int x)
{
return x&-x;
}
void update(int x,int k)
{
for(;x<=n;x+=lowbit(x))
c[x]+=k;
}
int query(int x)
{
int res=0;
for(;x>=1;x-=lowbit(x))
res+=c[x];
return res;
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
update(i,a[i]);
}
for(int i=1;i<=q;i++)
{
scanf("%d%d%d",&op,&x,&y);
if(op==1)
update(x,y);
else
printf("%d\n",query(y)-query(x-1));
}
return 0;
}