【模板】树状数组

1.概述

树状数组(Binary Indexed Trees),其基本用途是维护序列的前缀和。对于给定的序列 a [   ] a[\ ] a[ ],我们建立一个数组 c [   ] c[\ ] c[ ],其中 c [ x ] c[x] c[x] 保存序列 a [   ] a[\ ] a[ ] 的区间 [ x − l o w b i t ( x ) + 1 , x ] [x - lowbit(x) + 1, x] [xlowbit(x)+1,x] 中所有数的和。

事实上,数组 c [   ] c[\ ] c[ ] 可以看作一个如下图所示的树形结构,图中最下边一行是 N N N 个叶节点 ( N = 16 ) (N= 16) (N=16),代表数值 a [ 1 ∼ N ] a[1 \sim N] a[1N]。该结构满足以下性质:

  • 每个内部节点 c [ x ] c[x] c[x] 保存以它为根的子树中所有叶节点的和。

  • 每个内部节点 c [ x ] c[x] c[x] 的子节点个数等于 l o w b i t ( x ) lowbit(x) lowbit(x) 的位数。

  • 除树根外,每个内部节点 c [ x ] c[x] c[x] 的父节点是 c [ x + l o w b i t ( x ) ] c[x + lowbit(x)] c[x+lowbit(x)]

  • 树的深度为 O ( log ⁡ N ) O(\log N) O(logN)

  • 如果 N N N 不是 2 2 2 的整次幂,那么树状数组就是一个具有同样性质的森林结构。

在这里插入图片描述

2.原理

(1) 对于一个普通的二叉树,叶子结点代表数组 a [   ] a[\ ] a[ ] a [ 1 ] ∼ a [ 8 ] a[1] \sim a[8] a[1]a[8]

在这里插入图片描述

(2) 将其进行简单的变形。

在这里插入图片描述

(3) 然后定义每一列的顶端结点数组 c [   ] c[\ ] c[ ],令 c [ i ] c[i] c[i] 代表子树的叶结点的权值之和。

  • c [ 1 ] = a [ 1 ] c[1] = a[1] c[1]=a[1]
  • c [ 2 ] = a [ 1 ] + a [ 2 ] c[2] = a[1] + a[2] c[2]=a[1]+a[2]
  • c [ 3 ] = a [ 3 ] c[3] = a[3] c[3]=a[3]
  • c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c[4] = a[1] + a[2] + a[3] + a[4] c[4]=a[1]+a[2]+a[3]+a[4]
  • c [ 5 ] = a [ 5 ] c[5] = a[5] c[5]=a[5]
  • c [ 6 ] = a [ 5 ] + a [ 6 ] c[6] = a[5] + a[6] c[6]=a[5]+a[6]
  • c [ 7 ] = a [ 7 ] c[7] = a[7] c[7]=a[7]
  • c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] c[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

在这里插入图片描述

(4) 将数组 c [   ] c[\ ] c[ ] 的结点序号转化为二进制。

  • 1 = ( 001 ) , c [ 1 ] = a [ 1 ] 1 = (001), c[1] = a[1] 1=(001),c[1]=a[1]

  • 2 = ( 010 ) , c [ 2 ] = a [ 1 ] + a [ 2 ] 2 = (010), c[2] = a[1] + a[2] 2=(010),c[2]=a[1]+a[2]

  • 3 = ( 011 ) , c [ 3 ] = a [ 3 ] 3 = (011), c[3] = a[3] 3=(011),c[3]=a[3]

  • 4 = ( 100 ) , c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] 4 = (100), c[4] = a[1] + a[2] + a[3] + a[4] 4=(100),c[4]=a[1]+a[2]+a[3]+a[4]

  • 5 = ( 101 ) , c [ 5 ] = a [ 5 ] 5 = (101), c[5] = a[5] 5=(101),c[5]=a[5]

  • 6 = ( 110 ) , c [ 6 ] = a [ 5 ] + a [ 6 ] 6 = (110), c[6] = a[5] + a[6] 6=(110),c[6]=a[5]+a[6]

  • 7 = ( 111 ) , c [ 7 ] = a [ 7 ] 7 = (111), c[7] = a[7] 7=(111),c[7]=a[7]

  • 8 = ( 1000 ) , c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] 8 = (1000), c[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8] 8=(1000),c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

可以发现 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[i2k+1]+a[i2k+2]+......+a[i],其中, k k k i i i 的二进制中从最低位到高位连续零的长度。

可见,树状数组根本上来说就是二进制的应用。

要注意的是,树状数组只能计算 a [ 1 ] a[1] a[1] 开始的和, a [ 0 ] a[0] a[0] 这个元素是不能用的。

3.实现

3.1 lowbit(x)

l o w b i t ( x ) lowbit(x) lowbit(x) x x x 的二进制表达式中最低位的 1 1 1 所对应的值,换言之, l o w b i t ( x ) = 2 k lowbit(x)=2^k lowbit(x)=2k k k k i i i 的二进制中从最低位到高位连续零的长度。

对于一个数 x x x,想求 l o w b i t ( x ) lowbit(x) lowbit(x),可借助 x x x 的负数 − x -x x,进行按位与运算。

  • x = 6 = ( 0110 ) x = 6 = (0110) x=6=(0110),此时 k = 1 k=1 k=1

  • − x = − 6 = ( 1010 ) -x = -6 = (1010) x=6=(1010)

  • x x x & ( − x ) = ( 0010 ) = 2 1 (-x) = (0010) = 2^1 (x)=(0010)=21

// 返回x的二进制最右边1所对应的值
int lowbit(int x)
{
    return x & -x;
}

l o w b i t ( x ) lowbit(x) lowbit(x) 在树状数组中的应用:

c [ i ] = a [ i − 2 k + 1 ] + a [ i − 2 k + 2 ] + . . . . . . + a [ i ] = a [ i − l o w b i t ( i ) + 1 ] + a [ i − l o w b i t ( i ) + 2 ] + . . . . . . + a [ i ] \begin{aligned} c[i] & = a[i-2^k+1]+a[i-2^k+2]+......+a[i] \\ & = a[i-lowbit(i)+1]+a[i-lowbit(i)+2]+......+a[i] \end{aligned} c[i]=a[i2k+1]+a[i2k+2]+......+a[i]=a[ilowbit(i)+1]+a[ilowbit(i)+2]+......+a[i]

3.2 查询前缀和

树状数组支持的基本操作有两个,第一个操作是查询前缀和,即序列 a [   ] a[\ ] a[ ] 中第 1 ∼ x 1 \sim x 1x 个数的和。

首先求出 x x x 的二进制表示中每个等于 1 1 1 的位,然后把 [ 1 , x ] [1, x] [1,x] 分成 O ( log ⁡ N ) O(\log N) O(logN) 个小区间,而每个小区间的区间和都已经保存在数组 c [   ] c[\ ] c[ ] 中了。

下面代码在 O ( log ⁡ N ) O(\log N) O(logN) 的时间内查询前缀和:

// 返回a[1]+...+a[x]的和
int ask(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i))
    {
        res += c[i];
    }
    return res;
}

举例:求 a [ 1 ] + a [ 2 ] + . . . + a [ 7 ] a[1] + a[2] + ... + a[7] a[1]+a[2]+...+a[7] 的和

  • 7 ( 111 ) 7(111) 7(111)res += c[7]

  • 7 − l o w b i t ( 7 ) = 7 − l o w b i t ( 111 ) = 7 − 1 = 6 ( 110 ) 7-lowbit(7) = 7-lowbit(111) = 7-1 = 6(110) 7lowbit(7)=7lowbit(111)=71=6(110)res += c[6]

  • 6 − l o w b i t ( 6 ) = 6 − l o w b i t ( 110 ) = 6 − 2 = 4 ( 100 ) 6-lowbit(6) = 6-lowbit(110) = 6-2 = 4(100) 6lowbit(6)=6lowbit(110)=62=4(100)res += c[4]

  • 4 − l o w b i t ( 4 ) = 4 − l o w b i t ( 100 ) = 4 − 4 = 0 ( 000 ) 4-lowbit(4) = 4-lowbit(100) = 4-4 = 0(000) 4lowbit(4)=4lowbit(100)=44=0(000)

举例:求 a [ 1 ] + a [ 2 ] + . . . + a [ 5 ] a[1] + a[2] + ... + a[5] a[1]+a[2]+...+a[5] 的和

  • 5 ( 101 ) 5(101) 5(101)res += c[5]

  • 5 − l o w b i t ( 5 ) = 5 − l o w b i t ( 101 ) = 5 − 1 = 4 ( 100 ) 5-lowbit(5) = 5-lowbit(101) = 5-1 = 4(100) 5lowbit(5)=5lowbit(101)=51=4(100)res += c[4]

  • 4 − l o w b i t ( 4 ) = 4 − l o w b i t ( 100 ) = 4 − 4 = 0 ( 000 ) 4-lowbit(4) = 4-lowbit(100) = 4-4 = 0(000) 4lowbit(4)=4lowbit(100)=44=0(000)

当然,若要查询序列 a [   ] a[\ ] a[ ] 的区间 [ l , r ] [l,r] [l,r] 中所有数的和,只需计算 ask(r) - ask(l - 1)

3.3 单点增加

树状数组支持的第二个基本操作是单点增加,意思是给序列中的一个数 a [ x ] a[x] a[x] 加上 y y y,同时正确维护序列的前缀和。

根据上面给出的树形结构和它的性质,只有节点 c [ x ] c[x] c[x] 及其所有祖先节点保存的“区间和”包含 a [ x ] a[x] a[x],而任意一个节点的祖先至多只有 log ⁡ N \log N logN 个,我们逐一对它们的 c c c 值进行更新即可。

下面代码在 O ( log ⁡ N ) O(\log N) O(logN) 的时间内执行单点增加操作:

// 令a[x] += y
void add(int x, int y)
{
    for (int i = x; i <= n; i += lowbit(i))
    {
        c[i] += y;
    }
}

在这里插入图片描述

如图所示,当更新 a [ 1 ] a[1] a[1] 时,需要向上更新 c [ 1 ] c[1] c[1] c [ 2 ] c[2] c[2] c [ 4 ] c[4] c[4] c [ 8 ] c[8] c[8],则有:

  • 1 ( 001 ) 1(001) 1(001)c[1] += a[1]

  • 1 + l o w b i t ( 1 ) = 1 + l o w b i t ( 001 ) = 1 + 1 = 2 ( 010 ) 1+lowbit(1) = 1+lowbit(001) = 1+1 = 2(010) 1+lowbit(1)=1+lowbit(001)=1+1=2(010)c[2] += a[1]

  • 2 + l o w b i t ( 2 ) = 2 + l o w b i t ( 010 ) = 2 + 2 = 4 ( 100 ) 2+lowbit(2) = 2+lowbit(010) = 2+2 = 4(100) 2+lowbit(2)=2+lowbit(010)=2+2=4(100)c[4] += a[1]

  • 4 + l o w b i t ( 4 ) = 4 + l o w b i t ( 100 ) = 4 + 4 = 8 ( 1000 ) 4+lowbit(4) = 4+lowbit(100) = 4+4 = 8(1000) 4+lowbit(4)=4+lowbit(100)=4+4=8(1000)c[8] += a[1]

可以发现:更新过程是查询过程的逆过程。

4.初始化

在执行所有操作之前,我们需要对树状数组进行初始化,即针对原始序列 a [   ] a[\ ] a[ ] 构造一个树状数组 c [   ] c[\ ] c[ ]

为了简便,比较一般的初始化方法是:直接建立一个全为 0 0 0 的数组 c [   ] c[\ ] c[ ],然后对每个位置 x x x 执行 add(x, a[x]),就完成了对原始序列 a [   ] a[\ ] a[ ] 构造树状数组的过程,时间复杂度为 O ( N log ⁡ N ) O(N \log N) O(NlogN)通常采用这种初始化方法已经足够

更高效的初始化方法是:从小到大依次考虑每个节点 x x x,借助 l o w b i t lowbit lowbit 运算扫描它的子节点并求和。若采用这种方法,上面树形结构中的每条边只会被遍历一次,时间复杂度为 O ( N ) O(N) O(N)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值