树状数组

本文最后更新于:6 天前

代码量和线段树相比少了好多。

树状数组所能解决的问题使用线段树均能解决。

主要用于求解快速前缀和——在O(logn)的时间内完成单点修改区间查询(前缀和)。

树状数组理论上只能完成单点修改与区间查询,其他的问题均是转换为这一问题去处理。如(区间修改、单点修改 | 区间修改、区间查询)。

括号的内容太复杂了,y总说蓝桥杯不考…

​ --2022-3-31 17:28

1.存储组织

树状数组与原数组的关系可以用下图表示(图片来自博客):

image-20220330162018488

黑色数组代表原数组(a)。红色结构则代表树状数组©,存储的值是其全部子结点的值之和。可以看出:

c[1] = a[1]

c[2] = a[1] + a[2]

c[3] = a[3]

c[4] = a[1] + a[2] + a[3] + a[4]

c[8] = a[1] + a[2] + … + a[8]

我们定义kk为索引ii二进制表示中末尾连续0的个数。那么c[i]c[i]表示区间(a[i2k],a[i]](a[i - 2^k], a[i]]的所有元素和。

2.k的求解

为了建立树状数组,我们首先要求解出2k2^k。这里利用了负数在计算机中的存储特性来求解。

负数在计算机中以补码存储。对于运算x&(x)x \& (-x)

  • x=0x=0时,0&00 \& 0结果仍为0.
  • xx为奇数时,最后一位二进制数为1,取反加1后最后一位仍然为1,因此xxx-x除最后一位相同外,其余位均相反,即x&(x)=1x \& (-x) = 1。正好满足了20=12^0 = 1.
  • xx为偶数时,且为22mm次方时,此时xx最后有mm位0,当xx取反加一后,末尾正好有mm位0,其余位均为1,因此x&(x)=xx \& (-x) = x
  • xx为偶数,且不为2的mm次方时,可以将xx表示为一个奇数左移kk位,即x=y×(2k)x = y × (2 ^ k)。即xx的二进制表示下,最右边有连续kk个0,第k+1k+1位为1。取反加一后,最右侧仍为kk个0, 第k+1k+1位为1,其余位均与原数不同,因此x&(x)=2kx \& (-x) = 2^k

故,x&(x)x \& (-x)可以求得2k2^k的值。

3.索引关系

为了应付蓝桥杯,这里选择记住,特别是第二点:

  • 树状数组索引从1开始。
  • 索引为xx的节点,其父节点索引为x+lowbit(x)=x+(x&(x))x + lowbit(x) = x + (x \& (-x))

4.相关代码

我们使用aa数组表示原数组,使用treetree数组表示树状数组(索引从1至nn

4.1建树

我们将树状数组初始化为全0,那么建树的过程可以看作是为全零数组的每一个位置加上a[i]a[i]的过程。

1
for(int i = 1; i <= n; i++) add(i, a[i]);

4.2lowbit

1
2
3
static int lowbit(int x){
return x & (-x);
}

4.3查询queryquery

query(x)query(x)是要求出区间[1,x][1, x]的和。由于tree[x]tree[x]存储了a[xlowbit(x)+1]a[xlowbit(x)+2]...a[x]a[x - lowbit(x) + 1]、a[x - lowbit(x) + 2]、...、a[x]的和,因此只要将xx更新为xlowbit(x)x - lowbit(x),并重新执行query(xlowbit(x))query(x - lowbit(x)),如此递归直至x=0x = 0即可。

1
2
3
4
5
6
7
static int query(int x){
int sum = 0;
for(int i = x; i > 0; i = i - lowbit(i)){
sum += tree[i];
}
return sum;
}

4.4修改addadd

add(x,y)add(x, y)是将a[x]a[x]的值加上yy。在3.索引关系中提到,xx的父节点索引为x+lowbit(x)x + lowbit(x),因此通过不断的将xx更新为x+lowbit(x)x + lowbit(x),即可修改全部包含xx项的节点信息。

1
2
3
static void add(int x, int y){
for(int i = x; i <= n; i = i + lowbit(i)) tree[i] += y;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!