跳转至

树状数组

前置知识

位运算,二分,倍增,树

目标

树状数组的原理,基本操作

What

基本用途:维护序列的前缀和

 x = 7 x = 2^2 + 2^1 + 2^0任意正整数关于 2 的不重复次幂的唯一分解性质
区间[1, 7] 可以分解成 [1, 4][5, 6][7, 7] 三个小区间
长度分别是lowbit(4) = 4lowbit(6) = 2lowbit(7) = 1

类似上面的拆分
对于给定的序列 a我们建立一个数组 c其中 c[x] 保存序列 a 的区间 [x - lowbit(x) + 1] 中所有数的和

img

最下边一行是叶子结点,代表数值 a[1 ~ 16]。该结构满足以下性质:
1、每个内部结点 c[x] 保存以它为根的子树中的所有叶子结点的和
2、每个内部结点 c[x] 的子节点个数等于 lowbit(x) 的位数
3、除树根外,每个内部结点 c[x] 的父结点是 c[x + lowbit(x)]
4、树的深度为 O(logn)

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

基本操作,查询前缀和

int ask(int x) {
    int ans = 0;
    for (; x; x -= x & -x) ans += c[x];
    return ans;
}

区间 [l, r] 的区间和ask(r) - ask(l - 1)

基本操作,单点增加

给序列中的一个树 a[x] 加上 y同时正确维护序列的前缀和

根性树形结构和它的性质只有结点 c[x] 及其所有祖先结点保存的区间和包含 a[x]
任意一个结点的祖先至多只有 log(n) 
我们逐一对他们的 c 值进行更新即可

void add(int x, int y) {
    for (; x <= n; x += x & -x) c[x] += y;
}

基本操作,初始化

// O(nlogn)
for (int i = 1; i <= n; i++){
    cin >> a[i];
    add(i, a[i]); // 初始化时全部为0,依次修改
}

// 更优雅的初始化, O(n)
// 从小到大依次考虑每个结点 x,借助lowbit运算,扫码它的子节点并求和
// 这样,树形结构中的每条边,只会被遍历一次,时间复杂度为O(n)
int pre[N];
void init(){
    for (int i = 1; i <= n; i++){
        pre[i] = pre[i - 1] + a[i];
        c[i] = pre[i] - pre[i - lowbit(i)];
    }
}

总结

树状数组基于二进制划分与倍增思想

参考

《算法进阶指南》 树状数组 - OI Wiki