树状数组学习笔记

树状数组(Binary Index Tree, BIT)也是很多人心中最简洁优美的数据结构之一。最简单的树状数组支持两种操作,时间复杂度均为 :

  • 单点修改:更改数组中一个元素的值
  • 区间查询:查询一个区间内所有元素的和

当然,树状数组能维护的不局限于加法,支持的操作也不止这两种,甚至有大佬能用树状数组实现平衡树,但这篇笔记不会深入讨论(因为我也还不是很懂)。

树状数组的引入

回顾一下,我们说,我们要实现两种操作:单点修改区间求和。对于普通数组而言,单点修改的时间复杂度是 O ( 1 ) O(1) O(1),但区间求和的时间复杂度是 O ( n ) O(n) O(n)
当然,我们也可以用前缀和的方法维护这个数组,这样的话区间求和的时间复杂度就降到了 O ( 1 ) O(1) O(1),但是单点修改会影响后面所有的元素,时间复杂度是 O ( n ) O(n) O(n)
程序最后跑多长时间,是由最慢的一环决定的,因此现在我们希望找到这样一种折中的方法:无论单点修改还是区间查询,它都能不那么慢地完成。
注意到对 [ a , b ] [a,b] [a,b]进行区间查询只需查询 [ 1 , b ] [1,b] [1,b] [ 1 , a ) [1,a) [1,a)然后相减即可(前缀和就是这样进行区间查询的),所以我们可以把区间查询问题转化为求前n项和的问题。
关于数组的维护,有个很自然的想法:可以用一个数组维护若干个小区间,单点修改时,只更新包含这一元素的区间;求前n项和时,通过将区间进行组合,得到从1到n的区间,然后对所有用到的区间求和。实际上,设原数组是 A A A,如果 C i C_i Ci维护的区间是 [ A i , A i ] [A_i,A_i] [Ai,Ai] ,此结构就相当于普通数组(还浪费了一倍内存);如果 C i C_i Ci维护的区间就是 [ 1 , A i ] [1,A_i] [1,Ai],此结构就相当于前缀和。
现在我们试图寻找一种结构,一方面,单点修改时需要更新的区间不会太多;另一方面,区间查询时需要用来组合的区间也不会太多。
树状数组就是这样一种结构,它巧妙地利用了二进制(实际上,树状数组的英文名BIT,直译过来就是二进制下标树)。例如11,转化为二进制数就是 ( 1011 ) 2 (1011)_2 (1011)2,如果我们要求前11项和,可以分别查询 ( ( 0000 ) 2 , ( 1000 ) 2 ] ((0000)_2,(1000)_2] ((0000)2,(1000)2] ( ( 1000 ) 2 , ( 1010 ) 2 ] ((1000)_2,(1010)_2] ((1000)2,(1010)2]以及 ( ( 1010 ) 2 , ( 1011 ) 2 ] ((1010)_2,(1011)_2] ((1010)2,(1011)2]的和再相加。这三个区间怎么来的呢?其实就是不断地去掉二进制数最右边的一个1的过程。
我们定义,二进制数最右边的一个1,连带着它之后的0为 l o w b i t ( x ) lowbit(x) lowbit(x)(稍后再来看如何实现)。那么我们用 C i C_i Ci维护区间 ( ( A i − l o w b i t ( A i ) , A i ] ((A_i-lowbit(A_i),A_i] ((Ailowbit(Ai),Ai] ,这样显然查询前n项和时需要合并的区间数是少于 log ⁡ 2 n \log_2^n log2n的。
那么如何更新呢,大家会发现更新就是一个“爬树”的过程。一路往上更新,直到MAXN(树状数组的容量)。
每一步都把从右边起一系列连续的1变为0,再把这一系列1的前一位0变为1。这看起来像是一个进位的过程对吧?实际上,每一次加的正是 l o w b i t ( x ) lowbit(x) lowbit(x)。这样,我们更新的区间数不会超过 log ⁡ 2 M A X N \log_2^{MAXN} log2MAXN。一个能以 O ( log ⁡ n ) O(\log n) O(logn)时间复杂度进行单点修改和区间查询的数据结构就诞生了。

树状数组的实现

lowbit怎么算?如果一位一位验证的话,会形成额外的时间开销。然而,我们有这样神奇的一个公式:
l o w b i t ( x ) = ( x ) lowbit(x)=(x) lowbit(x)=(x) & ( − x ) (-x) (x)
我们需要知道,计算机里有符号数一般是以补码的形式存储的。-x相当于x按位取反再加1,会把结尾处原来1000…的形式,变成0111…,再变成1000…;而前面每一位都与原来相反。这时我们再把它和x按位与,得到的结果便是 l o w b i t ( x ) lowbit(x) lowbit(x)

单点修改

int tree[MAXN];
inline void update(int i, int x)
{
    for (int pos = i; pos < MAXN; pos += lowbit(pos))
        tree[pos] += x;
}

求前n项和

inline int query(int n)
{
    int ans = 0;
    for (int pos = n; pos; pos -= lowbit(pos))
        ans += tree[pos];
    return ans;
}

区间查询

inline int query(int a, int b)
{
    return query(b) - query(a - 1);
}

初始化的时候,我们只需要update每个点的初始值即可。

实战题目讲解链接:

(HDU P1166)敌兵布阵
(洛谷P1908) 逆序对

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值