1.
1.
首先,有一个问题:给定一个含有
n
n
个元素的集合,问题是怎样才能在比较短的时间内进行以下操作:
① 查询区间和:
Query(L,R)
Q
u
e
r
y
(
L
,
R
)
即计算
AL+AL+1+...+AR
A
L
+
A
L
+
1
+
.
.
.
+
A
R
② 单点查询和增加:得到 A[i] A [ i ] 或者在 A[i] A [ i ] 基础上增加 c c
③(拓展内容)区间增加:将一个区间的值统一增加
2. 2. 朴素的解决办法:对于区间和,可以利用前缀和思想,在 O(n) O ( n ) 内事先计算
那么
Query(L,R)=Sum[L]−Sum[R−1]
Q
u
e
r
y
(
L
,
R
)
=
S
u
m
[
L
]
−
S
u
m
[
R
−
1
]
,单次查询
O(1)
O
(
1
)
但是这样还是不够,于是就衍生出了一种叫做树状数组的数据结构来解决这类问题。树状数组可以做到一次操作
O(logn)
O
(
l
o
g
n
)
,
n
n
次就是。
3.
3.
树状数组
①引子:所谓数据结构,实现了对数据的一系列操作,比较高级的数据结构往往在数据的布局中隐含一些数学关系,通过这些对数学关系的利用,就实现了降低时空复杂度的目的。
②那么树状数组里隐藏了一些什么数学关系呢?我们可以先看看下图:
如图:这个看着像颗二叉树的东西,其实只是把编号的规则改了一下,仔细研究一下子结点和父节点编号的关系,比如4是8的左子节点,12是8的右子结点。有什么关系呢?
这几个数的二进制如下:
4:100
4
:
100
(左子)
12:1100
12
:
1100
(右子)
8:1000
8
:
1000
(父)
于是聪明的发明者就发现了
4+1002=8, 12−1002=8
4
+
100
2
=
8
,
12
−
100
2
=
8
(下标2表示二进制)那么这个
1002
100
2
怎么来的?
以
12
12
为例,可以看出
1002
100
2
是
12
12
二进制下最右边的
1
1
所对应的值,。比如,
76
76
的最右边的
1
1
所对应的值就是。
③
Lowbit
L
o
w
b
i
t
我们将
x
x
的二进制表达式最右边的所对应的值定义为一个函数叫
Lowbit(x)
L
o
w
b
i
t
(
x
)
,在程序实现中,Lowbit(x) = x & -x
,为啥会这样写呢?因为计算机中的整数采用补码表示,因此
−x
−
x
实际上是把
x
x
按位取反,然后末尾加的结果,比如:
二者按位“与”之后,前面部分为
0
0
,之后的保持不变,这样就得到了结果。
做完准备工作之后,我们来讲讲上图:
第一:可以发现,对于一个结点
i
i
,如果它是左子结点,那么它的父结点的编号就是
i+Lowbit(i)
i
+
L
o
w
b
i
t
(
i
)
;如果它是右子结点,那么它的父结点的编号就是
i−Lowbit(i)
i
−
L
o
w
b
i
t
(
i
)
(请在草稿上验证)。
第二:在图中,每一层的
Lowbit
L
o
w
b
i
t
值相同,并且
Lowbit
L
o
w
b
i
t
越大,越靠近树根。我用一些线段将这些灰色结点连了起来以便理解,需要注意的是编号为
0
0
的点是虚拟结点,为了方便理解而设定。
在搞清楚树是怎么构成的之后,开始解决问题,不过在这之前我们需要先构造一个数组,其中:
为什么会构造这么一个数组呢,可以从图中看出,每个灰色结点都有一个属于它的白色长条(对于 Lowbit=1 L o w b i t = 1 的点,就是它本身),而每一段“白色长条”所覆盖的结点中的数的总和就是 Ci C i 。例如: C12=A9+A10+A11+A12; C6=A5+A6 C 12 = A 9 + A 10 + A 11 + A 12 ; C 6 = A 5 + A 6 。(请在草稿上验证)
④计算前缀和
Si
S
i
:
根据
Ci
C
i
的性质,从图中可以看出,顺着某个结点
i
i
往上走(不一定经过树的边)一直到
0
0
,一路上把沿途的加上就行了。下面用图示来说明。
⑤单点增加
在这样的结构下修改一个
Ai
A
i
是会对其他结点有影响的,所以我们需要同时修改另一些结点,其实我们只需修改包含了
Ai
A
i
的点就行了,从
i
i
点往右上走(同样不一定经过树的边),沿途修改对应的就行,如图所示:
⑥代码:
单点增加:
void add(int x, int c){
while(x <= n){
C[x] += c;
x += lowbit(x);
}
}
区间求和:
int query(int x){//前缀和,区间和只需query[R]-query[L-1]即可
int ans = 0;
while(x > 0){
ans += C[x];
x -= lowbit(x);
}
return ans;
}
⑦:以上就介绍了树状数组支持的两个基本操作:“单点增加”+“区间查询”。那么,怎么用树状数组实现“区间增加”+“单点查询”呢?请见下文:
3.
3.
先给出一道例题:
①:给定长度为
n(n≤105)
n
(
n
≤
10
5
)
的数列
a
a
,然后输入行操作指令.
指令形如“
opt,l,r,c
o
p
t
,
l
,
r
,
c
”,
opt=1
o
p
t
=
1
表示把数列中第
l∼r
l
∼
r
个数都加
c
c
。
opt=0
o
p
t
=
0
表示询问
ar
a
r
的值(忽略
l,c
l
,
c
)
②思路分析:本题的指令是“区间增加”和“单点查询”,而树状数组仅支持“单点增加”,所以需要做一下转换。
新建一个数组
C[ ]
C
[
]
初始化为0,
C[ ]
C
[
]
就是
a
a
的差分,以此把区间操作改为单点操作。
对于指令一,转化为一下两个操作:
1.把加上
c
c
。
2.把减去
c
c
。
为什么会执行这两条呢,我们来考虑一下的前缀和,
1.对于
1≤x<l
1
≤
x
<
l
,前缀和不变。
2.对于
l≤x≤r
l
≤
x
≤
r
,前缀和增加了
c
c
。
3.对于,前缀和不变(
l
l
处加,
r+1
r
+
1
处减
n
n
,抵消了)
通过以上分析,可以发现数组的前缀和就反映了区间增加产生的影响。
于是,我们可以用树状数组维护
C
C
的前缀和,又因为这些操作具有累加性,所以在树状数组上查询前缀和,就得到了区间增加指令在
a[x]
a
[
x
]
上增加的数值总和。再加上
A[x]
A
[
x
]
的初始值,就可以得到单点查询的答案。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
int n;
int C[50005];
int lowbit(int x){
return x & -x;
}
int query(int x){
int ans = 0;
while(x > 0){
ans += C[x];
x -= lowbit(x);
}
return ans;
}
void add(int x, int c){
while(x <= n){
C[x] += c;
x += lowbit(x);
}
}
int main(){
scanf("%d", &n);
memset(C, 0, sizeof(C));
int b, e = 0;
for(int i = 1; i <= n; i++){
scanf("%d", &b);
add(i, b - e);
e = b;
}
for(int i = 1; i <= n; i++){
int opt, l, r, c;
scanf("%d %d %d %d", &opt, &l, &r, &c);
if(opt == 0){
add(l, c);
add(r + 1, -c);
}
else if(opt == 1){
printf("%d\n",query(r));
}
}
return 0;
}
文末总结:
本文借鉴了刘汝佳的《算法竞赛入门经典 训练指南》和李煴东的《算法竞赛进阶指南》。
个人所作,难免疏漏,希望大家发现问题能够指出,也希望大家能够从这篇文章中学到东西。