零、前言
树状数组巧妙了利用位运算和树形结构实现了允许单点修改的情况下,动态维护前缀和,并且实现单点修改和前缀和查询的效率的很可观。树状数组也可以对差分数组维护前缀和来实现区间修改区间查询,但由于过于繁琐,对于区间查询往往用线段树来代替,但树状数组以其简洁的代码实现成为了动态维护前缀和的不二选择。
一、lowbit
在介绍树状数组前需要先了解lowbit的概念,其作用会在后面树状数组中具体解释。
1.1 lowbit的定义
非负整数n在二进制表示下最低位1以及其后面的0构成的数值
例如: 44d = 10 1100b ,那么lowbit(44) = (100)b = 4(d)
1.2 lowbit的计算
我们手算补码的时候有一个技巧就是从符号位的下一位开始一直取反,到最后一个1的时候不对其取反然后停止,因为非符号位取反加一后原先的最后一个1又会变成1,于是我们可以利用这个技巧来快速的求lowbit。
我们将x和~x + 1做与运算就会得到lowbit,因为计算机中对一个数字取反后连符号位都会取反,而一个数字取反+1后原先的最低位1以及其后面的0不受影响,所以x 和 ~x + 1的公共部分就是lowbit
故有lowbit(x) = x &(~x + 1)
即然我们说了计算机中直接取反会对符号位也取反,那么~x + 1就会得到其相反数的补码,而计算机中以补码存储数字,故上面的式子可改进为lowbit(x) = x & -x
二、树状数组的思想
**树状数组(Binary Indexed Tree)**既然叫树状数组,那么它是怎么将数组抽象成树形结构的呢?
其实和汉明码类似,lowbit其实就是二进制的第一位为1、第二位为1、第三位为1…
而对于1到n这n个数字我们如果取二进制表示下含lowbit(x)的数字,我们发现这些数字正好是若干个长度为lowbit(x)的连续序列组成,每个序列间的间隔也是lowbit(x)
我们按照lowbit划分层次,每层以lowbit(x) * 1 ,lowbit(x) * 2 ,lowbit(x) * 3…结尾的长度为lowbit(x)的连续序列作为该层的节点,同时我们以t[ i ]代表当前层第i个序列的元素之和
而我们要维护前缀和的原序列就成了我们的叶子节点
于是有了下图的树形结构
这棵树有如下特点:
- t[x]保存以x为根节点的子树中叶节点值的和
- t[x]节点的长度等于lowbit(x)
- t[x] 的父节点为t[x + lowbit[x]]
- 整棵树的深度为logn + 1
- 我们发现t[x]满足如下公式:
t [ x ] = ∑ i = x − l o w b i t ( x ) + 1 x a [ i ] t[x] = \sum_{i = x - lowbit(x) + 1}^{x}a[i] t[x]=i=x−lowbit(x)+1∑xa[i]
三、树状数组的操作
由于区间修改我们有更适合的数据结构——线段树,所以我们就不介绍树状数组维护差分数组实现区间修改的操作了。
3.1 单点修改 update
当我们给一个叶子节点x增加k,那么需要对其父节点开始往上更新,而x父节点就是x + x & -x,所以一层循环就能搞定
void update(int x, int k)
{
for (; x <= n; x += x & -x)
t[x] += k;
}
时间复杂度O(logn)
3.2 前缀查询 query
查询前x个元素的前缀和,我们从图上看发现我们只需要从t[x]往左上的第一个节点走,路径上节点的值之和就是前x个节点的前缀和
而找到左上第一个节点我们只需要减去当前节点的lowbit值即可
而减去lowbit其实就是把最低位1置为0,而把最低位1置位0我们还可以通过x &= (x - 1)来实现
int query(int x)
{
int sum = 0;
for (; x > 0; x &= (x - 1))
sum += t[x];
return sum;
}
3.3 树状数组的建立 build
前面谈了单点修改和前缀查询,那么给定一个长度为n的数组我们如何建立树状数组呢?
暴力做法:进行n次插入操作,时间复杂度O(nlogn)
虽然可行,但是我们发现n次插入过程中其实是有冗余操作的,我们每次插入都会一直向上更新到根节点,但是我们这个过程可以等效为每次完成一个节点值的计算都对其父节点进行更新,这样最后我们的每个节点的值都是正确的,而且也只对数组遍历了一次,实现了O(n)建树
代码如下:
void build(const vector<int> &arr)
{
for (int i = 1; i <= n; i++)
{
t[i] += arr[i];
t[i + (i & -i)] += t[i];
}
}
3.4 二分求第k小
树状数组也可以类似于线段树那样,进行二分,不过这里求的是整体第k小。
对于线段树而言,左右儿子分别是自己区间的左半、右半。
我们观察树状数组的图,会发现什么?
我们0100为例,从lowbit = 4 往下的层,我们发现左半都不包含0100(除了4),右半都包含0100
这个性质使得我们可以二分求第k小,为了方便,我们再给个样例:
以上图为例,长度为8的树状数组,红色数字代表线段覆盖区间内的数字数目
我们要查询区间第15小
- 初始pos = 0,i = log2(8) = 3,当前查询到的数字个数cur = 0
- cur + tr[pos + (1 << i)] = 0 + tr[8] = 26 > 15,所以往左半区间跑:i –
- cur + tr[pos + (1 << i)] = 0 + tr[4] = 11 <= 15,所以往右半区间跑:cur += 11 = 11,pos += 4,i – | pos = 4
- cur + tr[pos + (1 << i)] = 11 + tr[6] = 16 > 15,所以往左半区间跑:i – | pos = 4
- cur + tr[pos + (1 << i)] = 11 + tr[3] = 12 <= 15,所以往右半区间跑:cur += 1 = 12,pos += 3,i – | pos = 5
- 此时 i = -1,已经超出范围了,查询结束,pos = 5
于是用更一般性的语言描述树状数组二分求区间第k小:
- 长度为n,查询第k小
- 初始化pos = 0,i = 2 ^ (log2(n)), 当前查询到的数字个数cur = 0
- 如果 pos + i <= n 并且 cur + tr[pos + i] <= k
- 则cur += tr[pos + i], 则pos += i
- i /= 2
当 i = 0,算法结束,pos就是找到的要找的数字
int select(int k) {
int x = 0;
int cur = 0;
for (int i = 1 << std::__lg(n); i; i /= 2) {
if (x + i <= n && cur + tr[x + i] <= k) {
x += i;
cur = cur + tr[x];
}
}
return x;
}
四、点修区查树状数组模板
template<typename T = int>
class FenWick {
private:
int n;
std::vector<T> tr;
public:
FenWick(int _n) : n(_n), tr(_n + 1)
{}
FenWick(const std::vector<T> &_init) : FenWick(_init.size()) {
init(_init);
}
void init(const std::vector<T> &_init) {
for (int i = 1; i <= n; ++ i) {
tr[i] += _init[i - 1];
int j = i + (i & -i);
if (j <= n)
tr[j] += tr[i];
}
}
void add(T x, T k) {
for (; x <= n; x += x & -x) tr[x] += k;
}
void add(T l, T r, T k) {
add(l, k), add(r + 1, -k);
}
T query(T x) const {
T res = T{};
for (; x; x &= x - 1) res += tr[x];
return res;
}
T query(T l, T r) const {
if (l > r) return T{};
return query(r) - query(l - 1);
}
int select(const T &k) {
int x = 0;
T cur{};
for (int i = 1 << std::__lg(n); i; i /= 2) {
if (x + i <= n && cur + tr[x + i] <= k) {
x += i;
cur = cur + tr[x];
}
}
return x;
}
};