树状数组

binary indexed tree(aka Fenwick tree)

一种简单而优美的数据结构

目录

  • 从 lowbit 说起
  • 用树状数组解决动态前缀和问题
  • 树状数组和树
  • 树状数组的应用
  • 树状数组的一般化

lowbit

  • 正整数 的二进制写法里最右边(也说最后)那个 称为 的 lowbit。

  • 函数 的二进制写法里最后那个 所在的数位的权值。例如:

    • 写成二进制是 ,最后那个 在第 位(从右往左数,从 开始数),所在数位的权值是 ,所以
    • 写成二进制是 ,所以
    • 写成二进制是 ,所以
  • 是能整除 的最高次方。

计算 lowbit(x)

  • 方法一:拿 试除。

  • 方法二:lowbit(x) 等于 x & -x

    int lowbit(int x) {
        return x & -x;
    }
    

x 和 -x 的编码

对正整数 的编码是把 的编码(即 的二进制写法)的每一位都取反再加一。

例:

  • int 类型的值 的编码是
  • 取反以后变成
  • 再加一又变成 ,这就是 的编码。

lowbit(x) 等于 x & -x 的原理

对比 的编码

的 lowbit 以下,包括 lowbit, 的编码相同,在 lowbit 以上, 的编码相反。

动态前缀和问题(Point Add Prefix Sum)

给你一个长为 的整数序列 。处理 个操作,操作有两种。

  • 给你一个下标 ,求
  • 给你一个整数 和下标 ,给 加上

考虑一个跟序列 一样长的序列

对于每个

也就是说 是从 开始往左数 项之和。

我们把这个序列 称为序列 树状数组

用树状数组 求序列 的前缀和

只要在 里从 开始往左找需要的项,加起来。

long long sum(int i) {
    long long ans = 0;
    while (i > 0) {
        ans += b[i];
        i -= lowbit(i); //或者写 i &= i - 1;
    }
    return ans;
}
  • 就是把 的 lowbit 去掉(即变成 )所得的数。
  • 在函数 sum 里,ans += b[i] 运行的次数就是 的二进制写法里 的个数。
  • sum(n) 的时间复杂度是

加上

  • 里,凡是含有 的项都给它加上

里(从左往右看)第一个含有 的项就是

之后,还有哪些项含有

lowbit 函数的一些性质

对于正整数

  • 如果 ,那么

证明都不困难。

之后,还有哪些项含有

  • 之后,从 都跟 不相交,而 包含

我们说 相交或者不相交,指的是 分别对应的序列 的那一段相交或者不相交。

由此可以推得

  • 之后的每一项要么和 不相交,要么包含
  • 之后下一个包含 的项是
  • 在序列 里,含有 的那些项是
    • ……

加上

int n; // n是序列A的长度
void add(int i, int x) {
    while(i <= n) {
        b[i] += x;
        i += lowbit(i); //或者写 i = (i | i - 1) + 1;
    }
}
  • 变成 ,lowbit 至少左移一位,所以函数 add 的时间是

  • 总之,树状数组可以在 的时间内处理动态前缀和问题里的两个操作。

用树状数组解动态区间和(Point Add Range Sum)

给你整数序列 。处理 个操作,操作有两种。

  • 给你两个下标 ,求
  • 给你一个整数 和下标 ,给 加上

区间和可以表为两个前缀和的差,有

构建树状数组

如何从序列 算出它的树状数组

法一:根据定义,算出 的前缀和序列,记作 ,那么 就等于

法二:假想一开始 的每一项都是 ,而把 的初始值通过加操作加给

树状数组和树

Q:为什么序列 被叫做树状数组,它和树有什么关系?

A:因为序列 隐含着树的结构。

树状数组其实是树

是包含 的“最小元素”,不妨把 看作 的父节点。

  • 一个矩形条表示一个 对应的 的那一段,箭头从一个 指向它的父节点。
  • 如果 那么 在从下往上数第 层(从 开始数)。

  • 如果 那么 个孩子,它们的下标是
  • 的所有孩子合起来再加上 恰好凑成

时间构建树状数组

依据上面两条性质,我们再给出两个构建树状数组的方法。

// 方法一
void build_fenwick(int n) {
    for (int i = 1; i <= n; i++) {
        b[i] += a[i];
        if (i + lowbit(i) <= n)
            b[i + lowbit(i)] += b[i];
    }
}
// 方法二
void build_fenwick_2(int n) {
    for (int i = 1; i <= n; i++) {
        for (int j = lowbit(i) / 2; j > 0; j /= 2)
            b[i] += b[i - j];
        b[i] += a[i];
    }
}

树状数组和线段树

  • 有人说,树状数组是线段树去掉所有右孩子。
  • 树状数组能做的,线段树都能做。
  • 但是树状数组胜在简单。

树状数组的应用

区间加(Range Add Point Get)

给你整数序列 。处理 个操作,操作有两种。

  • 给你两个下标 和一个整数 ,给每个 加上
  • 给你一个下标 ,求

考虑序列 差分序列 ,其中 )。不难看出,对于每个

也就是说,序列 是它的差分序列 的前缀和序列。给每个 加上 ,在差分序列上的效果是给 加上 ,给 减去

因此我们考虑维护差分序列 的树状数组。

区间加-区间和(Range Add Range Sum)

给你一个长为 的整数序列 。处理 个操作,操作有两种。

  • 给你两个下标 和一个整数 ,给每个 加上
  • 给你两个下标 ,求

为序列 的前缀和序列,即对每个

给每个 加上 ,在前缀和序列 上的效果是

  • 不变;
  • 对每个 ,给 加上
  • 对每个 ,给 加上

写成 。给每个 加上 ,在序列 上的效果是

  • 对每个 不变;
  • 对每个 ,给 加上 ,给 加上
  • 对每个 不变,给 加上

解法:分别维护 的差分序列的树状数组。

逆序数

对于一个长为 的序列 ,若整数对 满足 则称 的一个逆序 的全部逆序的数量称为 逆序数

给你一个长为 的整数序列 。求 的逆序数。

坐标压缩

  • 是不是逆序, 究竟是多少不重要,重要的是二者谁大谁小。
  • 中有 个不同的值,我们把 保序映射到 。例如把 变成 。这个处理称为坐标压缩

经过坐标压缩,我们有

更多人把这个处理称为「离散化」,这不恰当。

用树状数组计算逆序数

  • 对于每个 ,我们计算有多少个 使得 的逆序,或者说满足

  • 我们可以用树状数组对每个 算出有几个 满足
    如果有 个这样的 ,那么就有 使得 是逆序。

一般树状数组

回想我们是如何定义一个整数序列 的树状数组 ,然后利用 的前缀和的,要点是

  • 每个 的一段之和

的前缀和 可以表为 的若干项之和,是因为整数加法满足结合律

于是我们想到,

  • 序列 的元素不必限定为整数,前缀和里的加法运算也不必是整数相加。

我们尝试把树状数组推广到一般序列上。

是一个带有二元运算 的集合, 上的一个序列。

  • 运算满足结合律,就可以定义序列 的树状数组

    利用 求的 的前缀和。

  • 运算可逆,可以用 的区间和。

  • 运算满足交换律,对于把 修改为 )的操作,可以用前面 add 函数的方法去维护

习题

累积累积累积和

给你 。处理 个询问,询问有两种:

  • :把 改成
  • :令 。输出
限制

Rectangle Sum

二维平面上有 个带权值的点。第 个点的坐标是 ,权值是 。处理 个询问。

  • :求满足 的点的权值之和。
限制

序列移位操作

个格子排成一行。每一个格子里有一个整数。一开始,第 个格子里的数是 。处理 个询问

  • 给你一个整数 ),输出第 个格子的整数。把第 个格子里的整数拿出来,把第 到第 个格子里的数移到前一个格子里。把原来在第 个格子里的数放到第 个格子里。
限制

列队

军训时, 名学生站成一个 列的方阵。行列都从 开始编号。
最初,第 行第 列 的学生的编号是

在一天中发生了 次离队事件。每次有一个学生离队,方阵中出现一个空位;为了队伍的整齐,教官会依次下达两条指令:

  1. 向左看齐。这时第一列保持不动,所有学生向左填补空缺。
  2. 向前看齐。这时第一行保持不动,所有学生向前填补空缺。

教官规定不能有两个或更多学生同时离队。即在前一个离队的学生归队之后,下一个学生才能离队。因此在每一个离队的学生要归队时,队伍中有且仅有第 行第 列一个空位,这个学生会自然地填补到这个位置。

计算每一次离队事件中,离队的同学的编号是多少。

注意:学生的编号不会随着离队事件的发生而改变。

P3347

洛谷P1908