简介
树状数组是一种支持在 log n \log n logn 的时间内完成 单点修改 和 区间查询 的数据结构,并且它的 代码量极小。
事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的 代码要远比线段树短,时间效率常数也更小,因此很有学习价值。
有时,在差分数组和辅助数组的帮助下,树状数组还可解决更强的 区间加单点值 和 区间加区间和 问题。
首先你得知道,普通的树状数组维护的信息一定要满足 结合律 并且 可差分 ,如加法、乘法、异或等。
为什么是这样呢?因为事实上,树状数组维护的是一个类似前缀和的东西,所以维护的信息自然就要满足 结合律 并且 可差分。
查询
先考虑查询一个前缀区间 [ 1 , x ] [1,x] [1,x]。
首先有一个显然的结论,任意一个数都可以表示成若干个 2的整数次幂 相加的形式。
由此,查询时我们考虑把 [ 1 , x ] [1,x] [1,x] 拆成若干个 长度为2的整数次幂的区间 合并的形式。
现在问题是,怎么划分这些 长度为2的整数次幂的区间 ?
我们先考虑怎么找到这些区间的长度,这显然就简单了,转换为 二进制 就一目了然了。
举个例子, n = 10 n=10 n=10 时,转换成二进制就是 1010 ,很快能找到两个长度 1 0 2 10_2 102 和 100 0 2 1000_2 10002 ( x 2 x_2 x2 这表示在二进制下的 x x x)。
所以我们只要在 [ 1 , n ] [1,n] [1,n] 内酌情划分2个长度为 1 0 2 10_2 102 和 100 0 2 1000_2 10002 的区间就好了。
树状数组是这样划分的,它会把二进制的 从右往左看 ,每看到一个 1 就按照往左划分一个长度为这个 1 对应的位权的区间。
如,当 n = 11 n=11 n=11 时,它是这样划分成了这下三个区间 [ 11 , 11 ] , [ 9 , 10 ] , [ 1 , 9 ] [11,11],[9,10],[1,9] [11,11],[9,10],[1,9] 。
那么怎样找到这些被划分的区间呢?
设 t r x tr_x trx 管辖区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [x−lowbit(x)+1,x] ,其中 l o w b i t ( x ) lowbit(x) lowbit(x) 表示 x x x 在 二进制下从右往左看第一个 1 的位权 。
怎么计算
l
o
w
b
i
t
(
x
)
lowbit(x)
lowbit(x) 呢?由位运算知识可得 lowbit(x)=x&-x
。
原理如下,摘自 oiwiki
:
将
x
的二进制所有位全部取反,再加 1,就可以得到-x
的二进制编码。例如,6
的二进制编码是110
,全部取反后得到001
,加1
得到010
。设原先
x
的二进制编码是(...)10...00
,全部取反后得到[...]01...11
,加1
后得到[...]10...00
,也就是-x
的二进制编码了。这里x
二进制表示中第一个1
是x
最低位的1
。
(...)
和[...]
中省略号的每一位分别相反,所以x & -x = (...)10...00 & [...]10...00 = 10...00
,得到的结果就是lowbit
。
看完以上内容,你应该知道怎么查询了,从最右边的区间往左跳一下合并不就好了嘛。
其中 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [x−lowbit(x)+1,x] 的左边一个区间不就在 t r x − l o w b i t ( x ) tr_{x-lowbit(x)} trx−lowbit(x) 里嘛。
至于边界就是 x = 0 x=0 x=0 。
int getsum(int x){ //前缀区间和[1,x]
int res = 0;
for(; x > 0; x -= lowbit(x)) res += tr[x];
return res;
}
现在考虑查询一个区间
[
l
,
r
]
[l,r]
[l,r] ,因为维护的是 前缀区间并且满足结合律,所以结果就是 getsum(r)-getsum(l-1)
。
修改
考虑将 a a a 数组的第 x x x 个数加上某个数。
我们的目标是快速正确地维护 t r tr tr 数组。为保证效率,我们只需修改 管辖了 a [ x ] a[x] a[x] 的所有的 t r [ y ] tr[y] tr[y] ,因为其他的 t r [ y ] tr[y] tr[y] 显然 没有发生变化。
首先我们知道, t r [ x ] tr[x] tr[x] 肯定包含 a [ x ] a[x] a[x] ,而且 t r [ k ] ( k < x ) tr[k](k<x) tr[k](k<x) 肯定不包含 a [ x ] a[x] a[x] 。
所以可以确定如果 t r [ k ] tr[k] tr[k] 要包含 a [ x ] a[x] a[x] ,那么 x ≤ k x\le k x≤k 。
既然都已经知道 t r [ x ] tr[x] tr[x] 包含 a [ x ] a[x] a[x] 了,那么对于 t r [ k ] ( k > x ) tr[k](k>x) tr[k](k>x) , 你都在 x x x 的右边了 ,你管辖区间的长度至少得大于 l o w b i t ( x ) lowbit(x) lowbit(x) 吧(不理解自己画图)。
显然,往右看,第一个这样的 k = x + l o w b i t ( x ) k=x+lowbit(x) k=x+lowbit(x) 。
仔细思考,这相当于一个 二进制进位的过程。
你现在应该知道怎么修改了, x x x 往左跳再修改一下不就好了嘛。 其中边界显然就是 x > n x>n x>n 。
void update(int x, int k){ //将第x个元素加上k。
for(; x <= n; x += lowbit(x)) tr[x] += k;
}
建树
当然你可以用 update
n
log
n
n\log n
nlogn 建树,这里介绍两种
O
(
n
)
O(n)
O(n) 建树法。
以维护前缀和为例
方法一:直接按照 t r tr tr 的定义用前缀和优化。
方法二(类似单点修改):
因为 tr[i+lowbit(i)]
一定包含 tr[i]
(原理自行思考qwq)。
for (int i = 1; i <= n; ++i) {
tr[i] += a[i];
int j = i + lowbit(i);
if (j <= n) tr[j] += tr[i];
}
树?
Q:所以,这和树有甚么关系???
A:好像没有什么关系。
硬要扯,就是这样 (借用oiwiki
的图):