为什么要使用树状数组
比如说,我这里有一组数1, 2, 3, 2, …, k。我想知道第i到第j的和\(\mathop \sum \limits_{n = i}^j v[i]\)是多少?
朴素算法:
for (int k = 0; k < n; k++)
if (k >= i && k <= j) ans += v[k];
类似这种的写法,虽然在某些点值改变时也依然可以计算(我们称这种问题为动态问题),但复杂度最高到O(n),实在难以接受。
树状数组是通过前缀和思想,用来完成单点更新和区间查询的数据结构。它比之线段树,所用空间更小,速度更快,而且编程的复杂难度也大大减小。和ST相比,它可以动态更新数据。
前缀和
我们可以把查询区间和问题转化为前缀和问题。
前缀和是指:v[1]~v[i]的和,记作sum(i)。
如果我们能够计算s和t的前缀和,用sum(t)−sum(s−1)可以得到区间[s, t]的和。
而树状数组正是一个支持求前缀和的数据结构。
什么是树状数组
(如果你学过线段树,可以先从最后“树状数组的缺陷”看起,树状数组的灵感来源脉络会更加清晰)
我们依据二进制的思想,让”为1的最低位”在第一位的数(即奇数),储存长度为1的区间和(即v[i])。最低位在第二位的数,储存长度为2的区间和(v[i-1]+v[i])。这样,value数组储存了大大小小、长度为2^n 的区间和。
每一个长条形的块表示这个节点覆盖的区间。value[index]等于区间节点值之和。
这样做有什么好处呢?
1. 任意前缀和Sum(i)可以通过加运算得到。
2. 任意v[i]都可以通过减运算得到。
算法设计
注:后面出现的节点编号都是二进制数
- 如何求“最低位在哪一位”
举一个具体的运算过程:
操作 | 数值 | 表达式 |
---|---|---|
给定I | 10010 | i |
i取反 | 01101 | ~i |
加一 | 01110 | ~i+1 |
和i求交 | 00011 | & |
通过上面4步就可以求一个数的最低位在哪一位了!
我们先由此定义一个lowbit函数:lowbit(i) = 保留i最低位的1和更低位的0。
比如lowbit(100010) = 10
那么我们就可以具体推导出一些性质了:
1. 区间长度为1<<(lowbit(i)-1)
2. 区间是[i-lowbit(i)+1, i]
在具体程序中,lowbit(i) = i & -i。因为计算机里的整数采用补码表示,因此-x是x取反后+1的结果。
而对于节点编号为0110的节点,它恰好由一个长度为4的区间和一个长度为2的区间组成。第一个lowbit(i)表示它自身所覆盖的区间,而第二次lowbit(i-lowbit(i))表示它的前一个区间。同理,我们可以递归地求得所有它前面的区间。
BIT的求和
为了求得sum(i),将i通过二进制分解,不断让i = i - lowbit(i)。对于每个求得的i(包括初始i)求和value[i]就是sum(i)
BIT的更新
和线段树的更新不同,BIT的更新指的是v[i]加上a。所以需要让x[i] = a时,需要先-v[i]再+a。
对于更新操作,因为我们在节点储存部分和,所以当一个点更新后,覆盖这个点的父节点的值也要更新。我们的问题在于如何找到它的父节点,父节点的父节点等等,一直更新到顶部。
根据之前的铺垫,我们对BIT已经有了一定的认识,我们可以感性地猜想,一个节点的父节点t = i + lowbit(i)。想法是基于:通过进位使得覆盖的区间变大,而且可以证明, t - lowbit(t) + 1 <= i < t。i一定会被覆盖。
同理递归地更新上去就可以了。
事实上,我们更新的点都是2的幂,即形如1000的。所以每次加上lowbit,让低位全为0就可以。
具体算法
- 求和
ll b[MAX_N+1];
ll sum(int i) {//[1, i]
ll s = 0;
while (i > 0) {
s += b[i];
i -= i & -i;
}
return s;
}
- 更新
void add(int i, ll v) {
while (i <= N) {
b[i] += v;
i += i & -i;
}
}
树状数组的缺陷
其实谈到树状数组,有一个数据结构不能不提——线段树。
线段树的每个节点都储存了数据,但在计算和和更新值时,我们是不会用到右儿子的!如上图所示。
于是我们删除了线段树的所有右儿子,形成了树状数组。
但是这里有一个前提条件,也是树状数组的缺陷:节点的数据必须是可加减,而且要满足a + b = c, c -a = b
比如集合运算,是不能通过父节点和左子节点 来计算右子节点的。
题目总结
- LA 4329 Ping pong
- HDU 5975 Aninteresting game
参考文献
I. 《挑战程序设计竞赛 第二版》
II. 《算法竞赛 入门经典 训练指南》
这里是我的blog:有更多算法分享。排版可能也会更好看一点=v=
https://endlesslethe.com/binary-indexed-tree-tutorial.html