什么是树状数组:
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为 l o g ( n ) log(n) log(n)的数据结构。
它能用来干什么:
最经典的应用就是查询某一段区间元素的和,而且支持在线修改操作。如果没有修改操作,就没必要用树状数组了。
它和线段树有什么关系:
实际上树状数组就是没有右儿子的线段树,它实现起来比线段树简单,而且效率更高,空间占用的也更少。但是能用树状数组解决的问题,基本上都能用线段树解决。(反过来就不是了)
我还是不知道树状数组是什么:
不多解释了,先上几张图:
我们用数组A表示原序列:
A
1
、
A
2
…
…
A
n
A1、A2……An
A1、A2……An。
用数组
C
C
C代表树状数组,从上图中我们可以发现:
C
1
=
A
1
C1=A1
C1=A1
C
2
=
A
1
+
A
2
C2=A1+A2
C2=A1+A2
C
3
=
A
3
C3=A3
C3=A3
C
4
=
A
1
+
A
2
+
A
3
+
A
4
C4=A1+A2+A3+A4
C4=A1+A2+A3+A4
C
5
=
A
5
C5=A5
C5=A5
C
6
=
A
5
+
A
6
C6=A5+A6
C6=A5+A6
C
7
=
A
7
C7=A7
C7=A7
C
8
=
A
1
+
A
2
+
A
3
+
A
4
+
A
5
+
A
6
+
A
7
+
A
8
C8=A1+A2+A3+A4+A5+A6+A7+A8
C8=A1+A2+A3+A4+A5+A6+A7+A8
考虑数组
C
C
C下标的二进制表示与求和元素个数的关系:
C
1
C1
C1的下标1的二进制表示 1 末尾有0个连续的0 该节点管理1个元素
C
2
C2
C2的下标2的二进制表示 10 末尾有1个连续的0 该节点管理2个元素
C
3
C3
C3的下标3的二进制表示 11 末尾有0个连续的0 该节点管理1个元素
C
4
C4
C4的下标4的二进制表示 100 末尾有2个连续的0 该节点管理4个元素
……
由此我们得到一个规律:我们设
i
i
i的二进制表示的末尾有
k
k
k个连续的0,那么
C
i
Ci
Ci管理
2
k
2^k
2k个元素,即等于
2
k
2^k
2k个元素之和(这里的和只是运算符的一种形式,事实上问题并不一定是求和,所以用管理更为恰当)。那么有什么办法可以很快的算出来
2
k
2^k
2k呢?我们有一个名为
l
o
w
b
i
t
lowbit
lowbit的神奇操作:
l
o
w
b
i
t
(
x
)
=
x
&
(
−
x
)
lowbit(x)=x\&(-x)
lowbit(x)=x&(−x) 。它就是我们想要的答案。且对于节点
i
i
i,
i
+
l
o
w
b
i
t
(
i
)
i+lowbit(i)
i+lowbit(i)就是节点
i
i
i的父节点,
i
−
l
o
w
b
i
t
(
i
)
i-lowbit(i)
i−lowbit(i)就是节点
i
i
i的左兄弟节点。
知道这些有什么用呢?
我们可以对照上文中的图,若求 [ 1 , 7 ] [1,7] [1,7]的区间和,那么 s u m = C 7 + C 6 + C 4 sum=C_7+C_6+C_4 sum=C7+C6+C4,三次操作就得到了我们想要的结果,再看看 l o w b i t lowbit lowbit发挥了怎样的作用吧: 7 − l o w b i t ( 7 ) = 6 7-lowbit(7)=6 7−lowbit(7)=6、 6 − l o w b i t ( 6 ) = 4 6-lowbit(6)=4 6−lowbit(6)=4、 4 − l o w b i t ( 4 ) = 0 4-lowbit(4)=0 4−lowbit(4)=0,而 C 7 C_7 C7的左兄弟是 C 6 C_6 C6, C 6 C_6 C6的左兄弟是 C 4 C_4 C4,它们合起来就是区间 [ 1 , 7 ] [1,7] [1,7]的和。可以发现,我们求和的操作就是从区间右端点 r r r开始,不断地累加 C r C_r Cr,同时对 r r r作减去 l o w b i t lowbit lowbit操作。那么修改操作呢?看图会发现,对于任意的 A i A_i Ai,它影响到了多个 C j C_j Cj,即从叶子节点到根节点整条路径上的 C j C_j Cj,所以需要逆着来,即从要开始的子节点 i i i开始,不断修改 C i C_i Ci,同时对 i i i做加上 l o w b i t lowbit lowbit操作。有人可能会问,你这求的是 [ 1 , r ] [1,r] [1,r]的和啊,我要求 [ l , r ] [l,r] [l,r]的和怎么办呢?很简单,把 [ 1 , r ] [1,r] [1,r]和 [ 1 , l − 1 ] [1,l-1] [1,l−1]的和均求出来,然后用前者减去后者即可得到答案。
树状数组的基本操作:
这里我以求某段区间和作为例题来介绍一下树状数组的基本操作(注意树状数组的下标是从1开始的)。
l
o
w
b
i
t
lowbit
lowbit操作:
inline int lowbit(int x)
{
return x&(-x);
}
更新操作(单点修改),比如 a i a_i ai加上 v v v:
void update(int i,int v)
{
for(;i<=n;i+=lowbit(i))
tree[i]+=v;
}
查询操作,比如求 [ 1 , l ] [1,l] [1,l]的和:
int sum(int l)
{
int s=0;
for(;l;l-=lowbit(l))
s+=tree[l];
return s;
}
注意树状数组的初始化需要把元素逐一插入到树状数组中,所以初始化的复杂度是 O ( n l g n ) O(nlgn) O(nlgn)的。
二维树状数组
举一个简单的例子,我们有16个元素:
A
1
、
A
2
、
A
3
、
A
4
、
…
…
A
16
A1、A2、A3、A4、……A16
A1、A2、A3、A4、……A16构成了一个4*4的矩阵,我想查询某个矩阵内所有元素的和,并且可能会修改该矩阵的某个元素怎么做呢?朴素算法肯定是遍历统计和,复杂度
O
(
n
2
)
O(n^2)
O(n2),那有没有
O
(
(
l
g
n
)
2
)
O((lgn)^2)
O((lgn)2)的算法呢?有,利用二维树状数组。对于矩阵的每一行,我们用一个树状数组来管理,对于这
n
n
n个矩阵,我们用一个树状数组来管理。
我们先写出三个一维树状数组:
t
r
e
e
1
[
1
]
=
A
1
t
r
e
e
1
[
2
]
=
A
1
+
A
2
t
r
e
e
1
[
3
]
=
A
3
t
r
e
e
1
[
4
]
=
A
1
+
A
2
+
A
3
+
A
4
tree1[1]=A1 \ \ \ tree1[2]=A1+A2 \ \ \ \ \ tree1[3]=A3\ \ \ \ \ tree1[4]=A1+A2+A3+A4
tree1[1]=A1 tree1[2]=A1+A2 tree1[3]=A3 tree1[4]=A1+A2+A3+A4
t
r
e
e
2
[
1
]
=
A
5
t
r
e
e
2
[
2
]
=
A
5
+
A
6
t
r
e
e
2
[
3
]
=
A
7
t
r
e
e
2
[
4
]
=
A
5
+
A
6
+
A
7
+
A
8
tree2[1]=A5 \ \ \ tree2[2]=A5+A6 \ \ \ \ \ tree2[3]=A7\ \ \ \ \ tree2[4]=A5+A6+A7+A8
tree2[1]=A5 tree2[2]=A5+A6 tree2[3]=A7 tree2[4]=A5+A6+A7+A8
t
r
e
e
3
[
1
]
=
A
9
t
r
e
e
3
[
2
]
=
A
9
+
A
10
t
r
e
e
3
[
3
]
=
A
11
t
r
e
e
3
[
4
]
=
A
9
+
A
10
+
A
11
+
A
12
tree3[1]=A9 \ \ \ tree3[2]=A9+A10 \ \ \ tree3[3]=A11\ \ \ tree3[4]=A9+A10+A11+A12
tree3[1]=A9 tree3[2]=A9+A10 tree3[3]=A11 tree3[4]=A9+A10+A11+A12
……
然后再写出我们的二维树状数组:
T
r
e
e
[
1
]
[
1
]
=
A
1
Tree[1][1]=A1
Tree[1][1]=A1
T
r
e
e
[
1
]
[
2
]
=
A
1
+
A
2
Tree[1][2]=A1+A2
Tree[1][2]=A1+A2
T
r
e
e
[
1
]
[
3
]
=
A
3
Tree[1][3]=A3
Tree[1][3]=A3
T
r
e
e
[
1
]
[
4
]
=
A
1
+
A
2
+
A
3
+
A
4
Tree[1][4]=A1+A2+A3+A4
Tree[1][4]=A1+A2+A3+A4
T
r
e
e
[
2
]
[
1
]
=
A
1
+
A
5
Tree[2][1]=A1+A5
Tree[2][1]=A1+A5
T
r
e
e
[
2
]
[
2
]
=
A
1
+
A
2
+
A
5
+
A
6
Tree[2][2]=A1+A2+A5+A6
Tree[2][2]=A1+A2+A5+A6
T
r
e
e
[
2
]
[
3
]
=
A
3
+
A
7
Tree[2][3]=A3+A7
Tree[2][3]=A3+A7
T
r
e
e
[
2
]
[
4
]
=
A
1
+
A
2
+
A
3
+
A
4
+
A
5
+
A
6
+
A
7
+
A
8
Tree[2][4]=A1+A2+A3+A4+A5+A6+A7+A8
Tree[2][4]=A1+A2+A3+A4+A5+A6+A7+A8
T
r
e
e
[
3
]
[
1
]
=
A
9
Tree[3][1]=A9
Tree[3][1]=A9
T
r
e
e
[
3
]
[
2
]
=
A
9
+
10
Tree[3][2]=A9+10
Tree[3][2]=A9+10
T
r
e
e
[
3
]
[
3
]
=
A
11
Tree[3][3]=A11
Tree[3][3]=A11
T
r
e
e
[
3
]
[
4
]
=
A
9
+
A
10
+
A
11
+
A
12
Tree[3][4]=A9+A10+A11+A12
Tree[3][4]=A9+A10+A11+A12
……
也就是说
T
r
e
e
[
i
]
Tree[i]
Tree[i]维护的是:
t
r
e
e
i
+
t
r
e
e
(
i
−
1
)
+
…
…
+
t
r
e
e
(
i
−
l
o
w
b
i
t
(
i
)
+
1
)
treei+tree(i-1)+……+tree(i-lowbit(i)+1)
treei+tree(i−1)+……+tree(i−lowbit(i)+1) 比如上面
T
r
e
e
[
2
]
Tree[2]
Tree[2]维护的就是
t
r
e
e
2
tree2
tree2和
t
r
e
e
1
tree1
tree1。
那么我们求这个矩阵从第1行到第x行,第1列到第y列的操作就很简单了:(就是原来的一维变成了二维)。
求中间某个矩阵的和也可以使用类似一维树状数组的思想,把这个矩阵分为四个以左上角为原点的子矩阵。
void add(int x,int y,int v)
{
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=n;j+=lowbit(j))
tree[i][j]+=v;
}
int query(int x,int y)
{
int sum=0;
for(int i=x;i;i-=lowbit(i))
for(int j=y;j;j-=lowbit(j))
sum+=tree[i][j];
return sum;
}
树状数组处理 R M Q RMQ RMQ问题:
修改:
void updata(int x)
{
int lx,i;
while(x<=n)
{
h[x]=a[x];
lx=lowbit(x);
for(i=1;i<lx;i<<=1)
h[x]=max(h[x],h[x-i]);
x+=lowbit(x);
}
}
查询:
int query(int x,int y)
{
int ans=0;
while(y>=x)
{
ans=max(a[y],ans);
y--;
for(;y-lowbit(y)>=x;y-=lowbit(y))
ans=max(h[y],ans);
}
return ans;
}
针对区间 [ 1 , r ] [1,r] [1,r]的查询:
int query(int r)//查询[1,r]
{
int ans=INF;
while(r)
{
ans=min(ans,tree[r]);
r-=lowbit(r);
}
return ans;
}