树状数组介绍


一、树状数组是什么?

树状数组是一种支持 单点修改区间查询 的,代码量小的数据结构。

二、工作原理

1.初步感受

树状数组
其中a数组是原始数据数组,c数组可以看成管理a局部和的数组。例如
c 2 c_2 c2 管辖的是 a [ 1 … 2 ] a[1 \ldots 2] a[12]
c 4 c_4 c4 管辖的是 a [ 1 … 4 ] a[1 \ldots 4] a[14]
c 6 c_6 c6 管辖的是 a [ 5 … 6 ] a[5 \ldots 6] a[56]
c 8 c_8 c8 管辖的是 a [ 1 … 8 ] a[1 \ldots 8] a[18]
剩下的 c[x] 管辖的都是 a[x] 自己(可以看做 a [ x … x ] a[x \ldots x] a[xx] 的长度为 1 的小区间)。

2.求和举例

例如
a [ 1 … 7 ] a[1 \ldots 7] a[17]的前缀和为 a 1 + a 2 + a 3 + a 4 + a 5 + a 6 + a 7 a_1 + a_2 + a_3 + a_4 + a_5 + a_6 + a_7 a1+a2+a3+a4+a5+a6+a7 = c 4 + c 6 + c 7 c_4 +c_6 + c_7 c4+c6+c7
a [ 4 … 7 ] a[4 \ldots 7] a[47]的前缀和为 a [ 1 … 7 ] − a [ 1 … 3 ] a[1 \ldots 7] - a[1 \ldots 3] a[17]a[13] = c 4 + c 6 + c 7 − c 2 − c 3 c_4 +c_6 + c_7 - c_2 - c_3 c4+c6+c7c2c3

3.管辖区间

树状数组中,规定 c [ x ] c[x] c[x]管辖的区间长度为 2 k 2^{k} 2k,其中:

设二进制最低位为第 0 位,则 k k k 恰好为 x x x 二进制表示中,最低位的 1 所在的二进制位数;
2 k 2^k 2k c [ x ] c[x] c[x] 的管辖区间长度)恰好为 x x x 二进制表示中,最低位的 1 以及后面所有 0 组成的数

举个栗子
c 88 c_{88} c88 管辖的是哪个区间?
因为 8 8 ( 10 ) 88_{(10)} 88(10)= 0101100 0 ( 2 ) 01011000_{(2)} 01011000(2),其二进制最低位的 1 以及后面的 0 组成的二进制是 1000,即 8,所以 c 88 c_{88} c88 管辖 8 个 a a a 数组中的元素。因此, c 88 c_{88} c88 代表 a [ 81 … 88 ] a[81 \ldots 88] a[8188] 的区间信息。

我们记 x x x 二进制最低位 1 以及后面的 0 组成的数为 lowbit ⁡ ( x ) \operatorname{lowbit}(x) lowbit(x),那么 c [ x ] c[x] c[x] 管辖的区间就是 [ x − lowbit ⁡ ( x ) + 1 , x ] [x-\operatorname{lowbit}(x)+1, x] [xlowbit(x)+1,x]

注意这里的lowbit(x) = x & -x

int lowbit(int x) {
  return x & -x;
}

4.区间查询

其实任何一个区间查询都可以这么做:查询 a [ l … r ] a[l \ldots r] a[lr] 的和,就是 a [ 1 … r ] a[1 \ldots r] a[1r] 的和减去 a [ 1 … l − 1 ] a[1 \ldots l - 1] a[1l1] 的和,从而把区间问题转化为前缀问题,更方便处理。

那么如何做前缀查询,回顾一下查询 a [ 1 … 7 ] a[1 \ldots 7] a[17] 的过程:

c 7 c_{7} c7 往前跳,发现 c 7 c_{7} c7 只管辖 a 7 a_{7} a7 这个元素;然后找 c 6 c_{6} c6,发现 c 6 c_{6} c6 管辖的是 a [ 5 … 6 ] a[5 \ldots 6] a[56],然后跳到 c 4 c_{4} c4,发现 c 4 c_{4} c4 管辖的是 a [ 1 … 4 ] a[1 \ldots 4] a[14]这些元素,然后再试图跳到 c 0 c_0 c0,但事实上 c 0 c_0 c0 不存在,不跳了。

我们刚刚找到的 c c c c 7 , c 6 , c 4 c_7, c_6, c_4 c7,c6,c4,事实上这就是 a [ 1 … 7 ] a[1 \ldots 7] a[17]拆分出的三个小区间,合并一下,答案是 c 7 + c 6 + c 4 c_7 + c_6 + c_4 c7+c6+c4

我们可以写出查询 a [ 1 … x ] a[1 \ldots x] a[1x] 的过程:

c [ x ] c[x] c[x] 开始往前跳,有 c [ x ] c[x] c[x] 管辖 a [ x − lowbit ⁡ ( x ) + 1 … x ] a[x-\operatorname{lowbit}(x)+1 \ldots x] a[xlowbit(x)+1x]
x ← x − lowbit ⁡ ( x ) x \gets x - \operatorname{lowbit}(x) xxlowbit(x),如果 x = 0 x = 0 x=0 说明已经跳到尽头了,终止循环;否则回到第一步。将跳到的 c c c 合并。

int getsum(int x) {  // a[1]..a[x]的和
  int ans = 0;
  while (x > 0) {
    ans = ans + c[x];
    x = x - lowbit(x);
  }
  return ans;
}

5.单点修改

现在来考虑如何单点修改 a [ x ] a[x] a[x]。我们的目标是快速正确地维护 c c c 数组。为保证效率,我们只需遍历并修改管辖了 a [ x ] a[x] a[x] 的所有 c [ y ] c[y] c[y],因为其他的 c c c 显然没有发生变化。注意 c [ y ] c[y] c[y]必定包含 c [ x ] c[x] c[x]

n n n 表示 a a a 的大小,不难写出单点修改 a [ x ] a[x] a[x] 的过程:

初始令 x ′ = x x' = x x=x
修改 c [ x ′ ] c[x'] c[x]
x ′ ← x ′ + lowbit ⁡ ( x ′ ) x' \gets x' + \operatorname{lowbit}(x') xx+lowbit(x),如果 x ′ > n x' > n x>n 说明已经跳到尽头了,终止循环;否则回到第二步。

void add(int x, int k) {
  while (x <= n) {  // 不能越界
    c[x] = c[x] + k;
    x = x + lowbit(x);
  }
}

总结

树状数组有很多变种,这里不一一赘述,如有兴趣,可以参考https://oi.wiki/ds/fenwick/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值