树状数组
一个包含n个数的序列2,7,1,125,…计算前i个数的和值,即前缀和query[i]=a[1]+a[2]…+a[i] (1,2…n) 。该怎么计算呢?
累加求前n个数的和值需要O(n)时间。而且若对a[i]进行修改,则query[i],query[1]… query[m]都需要修改,最坏的情况下需要O(n)时间。
树状数组可以高效实现,其查询前缀和与点更新均为O(logn)。那么树状数组是如何巧妙地实现呢?
树状数组引入了分级管理制度,设置一个管理小组,每个管理员管理
一个或多个连续的元素。例如,数列有9个元素,分别用a[1],a[2]…a[9]存储,管理数组为c[]。管理数组c[]是树状的,因此称为树状数组
。
树状数组,又称为二进制索引树
(Binary Indexed Trees),通过二进制分解划分区间。那么c[i]存储的是哪些值?
1、区间长度
若i的二进制表示末尾有k个连续的0,则e[i]存储的区间长度为 2 k 2^k 2k,从a[i]向前数 2 k 2^k 2k个元素,即 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]。
区间长度就是i的二进制表示下最低位的1及它后面的0构成的数值。例如i=20,其二进制表示为10100,末尾有2个0,区间长度为 2 2 2^2 22,其实就是10100最低位的1及其后面的O构成的数值 ( 100 ) 2 (100)~_2 (100) 2,十进制为4。
计算区间长度的方法为 i & ( ( ∼ i ) + 1 ) i\&((\sim i)+1) i&((∼i)+1)。在计算机中二进制数采用的是补码表示,-i的补码正好是i取反加1。所以c[i]存储的区间长度: l o w b i t ( i ) = ( − i ) & i lowbit(i)=(-i) \& i lowbit(i)=(−i)&i
2、前驱和后继
直接前驱
:c[i]的直接前驱为c[i-lowbit(i)],即e[i]左侧紧邻的子树的根。
直接后继
:c[i]的直接后继为c[i+lowbit(i)],即c[i]的父节点。
前驱
: c[i]左侧所有子树的根。
后继
: c[i]的所有祖先。
3、查询前缀和
前i个元素的前缀和query[i]等于c[i]加上c[i]的前驱。如:
query[7]等于c[7]加上c[7]的前驱,query[7]=c[7]+c[6]+c[4]。
4、点更新
若对a[i]进行修改,令a[i]加上一个数z,则只需更新c[i]及其后继(祖先),即令这些节点都加上z即可,无需修改其他节点。
例如,修改a[5],令其加2。只需c[5]+2,然后c[5]的后继分别加2,即c[6]+2、c[8]+2。
5、查询区间和
若求区间和值a[i]+a[i+1]+ …+a[j],则求解前j个元素的和值减去前i-1个元素的和值即可,即query[j]-query[i-1]。
s
u
m
[
j
]
−
s
u
m
[
i
−
1
]
=
a
[
i
]
+
a
[
i
+
1
]
+
.
.
.
+
a
[
j
]
sum[j]-sum[i-1]=a[i]+a[i+1]+...+a[j]
sum[j]−sum[i−1]=a[i]+a[i+1]+...+a[j]
算法分析:
点更新时,从叶子更新到树根,执行的次数不超过树的高度O(logn)。
前缀和查询时,从当前结点一直查找前驱,前驱的个数不超过O(logn)。
树状数组的局限性
树状数组主要用于查询前缀和
,区间
和及点更新
,对点查询、区间修改效率较低。
前缀和
:求a[1]…a[i]的前缀和,普通数组O(n),树状数组O(logn)。
区间和
:求a[i]…a[j]的区间和,普通数组O(n),树状数组O(logn)。
点更新
:修改a[i]加上z,普通数组O(1),树状数组O(logn)。
点查询:查找第i个元素,普通数组O(1),树状数组O(logn) (求query[i]-query[i-1])。
区间更新:区间a[i]…a[j]的所有元素加z,普通数组O(n),树状数组O(nlogn) 。
代码
#include <bits/stdc++.h>
#define N 100
using namespace std;
int c[N];
int lowbit(int i) {
return i&(-i);
}
//前缀和
int query(int i) {
if(i) return c[i]+query(i-lowbit(i));
return 0;
}
// 区间和
int query(int s,int e) {
return query(e)-query(s-1);
}
// 点更新,c[i]加上z
void update(int i,int z,int n) {
for(; i<=n; i+=lowbit(i)) c[i]+=z;
}
int main() {
int a[10]= {1,2,3,4,5,6,7,8,9};
int n=sizeof(a)/sizeof(a[0]);
//生成树状数组
for(int i=1; i<=n; i++) update(i,a[i-1],n);
return 0;
}
例题