跳转至

树状数组

引入

树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。

什么是「单点修改」和「区间查询」?

假设有这样一道题:

已知一个数列 ,你需要进行下面两种操作:

  • 给定 ,将 自增 。
  • 给定 ,求解 的和。

其中第一种操作就是「单点修改」,第二种操作就是「区间查询」。

类似地,还有:「区间修改」、「单点查询」。它们分别的一个例子如下:

  • 区间修改:给定 ,将 中的每个数都分别自增 ;
  • 单点查询:给定 ,求解 的值。

注意到,区间问题一般严格强于单点问题,因为对单点的操作相当于对一个长度为 的区间操作。

普通树状数组维护的信息及运算要满足 结合律 且 可差分,如加法(和)、乘法(积)、异或等。

  • 结合律:,其中 是一个二元运算符。
  • 可差分:具有逆运算的运算,即已知 和 可以求出 。

需要注意的是:

  • 模意义下的乘法若要可差分,需保证每个数都存在逆元(模数为质数时一定存在);
  • 例如 , 这些信息不可差分,所以不能用普通树状数组处理,但是:

事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小,因此仍有学习价值。

有时,在差分数组和辅助数组的帮助下,树状数组还可解决更强的 区间加单点值 和 区间加区间和 问题。

树状数组

初步感受

先来举个例子:我们想知道 的前缀和,怎么做?

一种做法是:,需要求 个数的和。

但是如果已知三个数 ,,, 的和, 的总和, 的总和(其实就是 自己)。你会怎么算?你一定会回答:,只需要求 个数的和。

这就是树状数组能快速求解信息的原因:我们总能将一段前缀 拆成 不多于 段区间,使得这 段区间的信息是 已知的。

于是,我们只需合并这 段区间的信息,就可以得到答案。相比于原来直接合并 个信息,效率有了很大的提高。

不难发现信息必须满足结合律,否则就不能像上面这样合并了。

下面这张图展示了树状数组的工作原理:

最下面的八个方块代表原始数据数组 。上面参差不齐的方块(与最上面的八个方块是同一个数组)代表数组 的上级—— 数组。

数组就是用来储存原始数组 某段区间的和的,也就是说,这些区间的信息是已知的,我们的目标就是把查询前缀拆成这些小区间。

例如,从图中可以看出:

  • 管辖的是 ;
  • 管辖的是 ;
  • 管辖的是 ;
  • 管辖的是 ;
  • 剩下的 管辖的都是 自己(可以看做 的长度为 的小区间)。

不难发现, 管辖的一定是一段右边界是 的区间总信息。我们先不关心左边界,先来感受一下树状数组是如何查询的。

举例:计算 的和。

过程:从 开始往前跳,发现 只管辖 这个元素;然后找 ,发现 管辖的是 ,然后跳到 ,发现 管辖的是 这些元素,然后再试图跳到 ,但事实上 不存在,不跳了。

我们刚刚找到的 是 ,事实上这就是 拆分出的三个小区间,合并得到答案是 。

举例:计算 的和。

我们还是从 开始跳,跳到 再跳到 。此时我们发现它管理了 的和,但是我们不想要 这一部分,怎么办呢?很简单,减去 的和就行了。

那不妨考虑最开始,就将查询 的和转化为查询 的和,以及查询 的和,最终将两个结果作差。

管辖区间

那么问题来了, 管辖的区间到底往左延伸多少?也就是说,区间长度是多少?

树状数组中,规定 管辖的区间长度为 ,其中:

  • 设二进制最低位为第 位,则 恰好为 二进制表示中,最低位的 1 所在的二进制位数;
  • ( 的管辖区间长度)恰好为 二进制表示中,最低位的 1 以及后面所有 0 组成的数。

举个例子, 管辖的是哪个区间?

因为 ,其二进制最低位的 1 以及后面的 0 组成的二进制是 1000,即 ,所以 管辖 个 数组中的元素。

因此, 代表 的区间信息。

我们记 二进制最低位 1 以及后面的 0 组成的数为 ,那么 管辖的区间就是 。

这里注意: 指的不是最低位 1 所在的位数 ,而是这个 1 和后面所有 0 组成的 。

怎么计算 lowbit?根据位运算知识,可以得到 lowbit(x) = x & -x。

lowbit 的原理

将 x 的二进制所有位全部取反,再加 1,就可以得到 -x 的二进制编码。例如, 的二进制编码是 110,全部取反后得到 001,加 1 得到 010。

设原先 x 的二进制编码是 (...)10...00,全部取反后得到 [...]01...11,加 1 后得到 [...]10...00,也就是 -x 的二进制编码了。这里 x 二进制表示中第一个 1 是 x 最低位的 1。

(...) 和 [...] 中省略号的每一位分别相反,所以 x & -x = (...)10...00 & [...]10...00 = 10...00,得到的结果就是 lowbit。

实现
1
2
3
4
5
6
7
8
int lowbit(int x) {
  // x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
  // lowbit(0b01011000) == 0b00001000
  //          ~~~~^~~~
  // lowbit(0b01110010) == 0b00000010
  //          ~~~~~~^~
  return x & -x;
}
1
2
3
4
5
6
7
8
9
def lowbit(x):
    """
    x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
    lowbit(0b01011000) == 0b00001000
            ~~~~~^~~
    lowbit(0b01110010) == 0b00000010
            ~~~~~~~^~
    """
    return x & -x

区间查询

接下来我们来看树状数组具体的操作实现,先来看区间查询。

回顾查询 的过程,我们是将它转化为两个子过程:查询 和查询 的和,最终作差。

其实任何一个区间查询都可以这么做:查询 的和,就是 的和减去 的和,从而把区间问题转化为前缀问题,更方便处理。

事实上,将有关 的区间询问转化为 和 的前缀询问再差分,在竞赛中是一个非常常用的技巧。

那前缀查询怎么做呢?回顾下查询 的过程:

从 往前跳,发现 只管辖 这个元素;然后找 ,发现 管辖的是 ,然后跳到 ,发现 管辖的是 这些元素,然后再试图跳到 ,但事实上 不存在,不跳了。

我们刚刚找到的 是 ,事实上这就是 拆分出的三个小区间,合并一下,答案是 。

观察上面的过程,每次往前跳,一定是跳到现区间的左端点的左一位,作为新区间的右端点,这样才能将前缀不重不漏地拆分。比如现在 管的是 ,下一次就跳到 ,即访问 。

我们可以写出查询 的过程:

  • 从 开始往前跳,有 管辖 ;
  • 令 ,如果 说明已经跳到尽头了,终止循环;否则回到第一步。
  • 将跳到的 合并。

实现时,我们不一定要先把 都跳出来然后一起合并,可以边跳边合并。

比如我们要维护的信息是和,直接令初始 ,然后每跳到一个 就 ,最终 就是所有合并的结果。

实现
1
2
3
4
5
6
7
8
int getsum(int x) {  // a[1]..a[x]的和
  int ans = 0;
  while (x > 0) {
    ans = ans + c[x];
    x = x - lowbit(x);
  }
  return ans;
}
1
2
3
4
5
6
def getsum(x): # a[1]..a[x]的和
    ans = 0
    while x > 0:
        ans = ans + c[x]
        x = x - lowbit(x)
    return ans

树状数组与其树形态的性质

在讲解单点修改之前,先讲解树状数组的一些基本性质,以及其树形态来源,这有助于更好理解树状数组的单点修改。

我们约定:

  • 。即, 是 管辖范围的左端点。
  • 对于任意正整数 ,总能将 表示成 的形式,其中 。
  • 下面「 和 不交」指 的管辖范围和 的管辖范围不相交,即 和 不相交。「 包含于 」等表述同理。

性质 :对于 ,要么有 和 不交,要么有 包含于 。

证明

证明:假设 和 相交,即 和 相交,则一定有 。

将 表示为 ,则 。所以, 可以表示为 ,其中 。

不难发现 。又因为 ,

所以 ,即 。

所以,如果 和 相交,那么 的管辖范围一定完全包含于 。

性质 :在 真包含于 。

证明

证明:设 ,,则 ,。

不难发现 ,所以 ,即 。

所以, 真包含于 。

性质 :对于任意 ,有 和 不交。

证明

证明:设 ,则 ,其中 。

不难发现 。又因为 ,

因此 ,即 。

所以, 和 不交。

有了这三条性质的铺垫,我们接下来看树状数组的树形态(请忽略 向 的连边)。

事实上,树状数组的树形态是 向 连边得到的图,其中 是 的父亲。

注意,在考虑树状数组的树形态时,我们不考虑树状数组大小的影响,即我们认为这是一棵无限大的树,方便分析。实际实现时,我们只需用到 的 ,其中 是原数组长度。

这棵树天然满足了很多美好性质,下面列举若干(设 表示 的直系父亲):

  • 。
  • 大于任何一个 的后代,小于任何一个 的祖先。
  • 点 的 严格小于 的 。
证明

设 ,,则 ,不难发现 ,证毕。

  • 点 的高度是 ,即 二进制最低位 1 的位数。
高度的定义

点 的高度 满足:如果 ,则 ,否则 ,其中 代表 的所有儿子(此时 至少存在一个儿子 )。

也就是说,一个点的高度恰好比它最高的那个儿子再高 。如果一个点没有儿子,它的高度是 。

这里引出高度这一概念,是为后面解释复杂度更方便。

  • 真包含于 (性质 )。
  • 真包含于 ,其中 是 的任一祖先(在上一条性质上归纳)。
  • 真包含 ,其中 是 的任一后代(上面那条性质 , 颠倒)。
  • 对于任意 ,若 不是 的祖先,则 和 不交。
证明

和 的祖先中,一定存在一个点 使得 ,根据性质 得 不相交于 ,而 包含 ,因此 不交于 。

  • 对于任意 ,如果 不在 的子树上,则 和 不交(上面那条性质 , 颠倒)。
  • 对于任意 ,当且仅当 是 的祖先, 真包含于 (上面几条性质的总结)。这就是树状数组单点修改的核心原理。
  • 设 ,则其儿子数量为 ,编号分别为 。
    • 举例:假设 , 的二进制编号为 ...1000,则 有三个儿子,二进制编号分别为 ...0111、...0110、...0100。
证明

在一个数 的基础上减去 , 二进制第 位会反转,而更低的位保持不变。

考虑 的儿子 ,有 ,即 且 。设 。

考虑 , 的第 位及后方均为 ,所以 的第 位变为 ,后面仍为 ,满足 。

考虑 ,则 , 的第 位变为 ,不满足 。

考虑 ,则 , 的第 位是 ,所以 ,不满足 。

  • 的所有儿子对应 的管辖区间恰好拼接成 。
    • 举例:假设 , 的二进制编号为 ...1000,则 有三个儿子,二进制编号分别为 ...0111、...0110、...0100。
    • c[...0100] 表示 a[...0001 ~ ...0100]。
    • c[...0110] 表示 a[...0101 ~ ...0110]。
    • c[...0111] 表示 a[...0111 ~ ...0111]。
    • 不难发现上面是三个管辖区间的并集恰好是 a[...0001 ~ ...0111],即 。
证明

的儿子总能表示成 ,不难发现, 越小, 越大,代表的区间越靠右。我们设 ,则 分别构成 从左到右的儿子。

不难发现 ,所以 。

考虑相邻的两个儿子 和 。前者管辖区间的右端点是 ,后者管辖区间的左端点是 ,恰好相接。

考虑最左面的儿子 ,其管辖左边界 恰为 。

考虑最右面的儿子 ,其管辖右边界就是 。

因此,这些儿子的管辖区间可以恰好拼成 。

单点修改

现在来考虑如何单点修改 。

我们的目标是快速正确地维护 数组。为保证效率,我们只需遍历并修改管辖了 的所有 ,因为其他的 显然没有发生变化。

管辖 的 一定包含 (根据性质 ),所以 在树状数组树形态上是 的祖先。因此我们从 开始不断跳父亲,直到跳得超过了原数组长度为止。

设 表示 的大小,不难写出单点修改 的过程:

  • 初始令 。
  • 修改 。
  • 令 ,如果 说明已经跳到尽头了,终止循环;否则回到第二步。

区间信息和单点修改的种类,共同决定 的修改方式。下面给几个例子:

  • 若 维护区间和,修改种类是将 加上 ,则修改方式则是将所有 也加上 。
  • 若 维护区间积,修改种类是将 乘上 ,则修改方式则是将所有 也乘上 。

然而,单点修改的自由性使得修改的种类和维护的信息不一定是同种运算,比如,若 维护区间和,修改种类是将 赋值为 ,可以考虑转化为将 加上 。如果是将 乘上 ,就考虑转化为 加上 。

下面以维护区间和,单点加为例给出实现。

实现
1
2
3
4
5
6
void add(int x, int k) {
  while (x <= n) {  // 不能越界
    c[x] = c[x] + k;
    x = x + lowbit(x);
  }
}
1
2
3
4
def add(x, k):
    while x <= n: # 不能越界
        c[x] = c[x] + k
        x = x + lowbit(x)

建树

也就是根据最开始给出的序列,将树状数组建出来( 全部预处理好)。

一般可以直接转化为 次单点修改,时间复杂度 (复杂度分析在后面)。

比如给定序列 要求建树,直接看作对 单点加 ,对 单点加 ,对 单点加 即可。

也有 的建树方法,见本页面 建树 一节。

复杂度分析

空间复杂度显然 。

时间复杂度:

  • 对于区间查询操作:整个 的迭代过程,可看做将 二进制中的所有 ,从低位到高位逐渐改成 的过程,拆分出的区间数等于 二进制中 的数量(即 )。因此,单次查询时间复杂度是 ;
  • 对于单点修改操作:跳父亲时,访问到的高度一直严格增加,且始终有 。由于点 的高度是 ,所以跳到的高度不会超过 ,所以访问到的 的数量是 级别。因此,单次单点修改复杂度是 。

区间加区间和

前置知识:前缀和 & 差分。

该问题可以使用两个树状数组维护差分数组解决。

考虑序列 的差分数组 ,其中 。由于差分数组的前缀和就是原数组,所以 。

一样地,我们考虑将查询区间和通过差分转化为查询前缀和。那么考虑查询 的和,即 ,进行推导:

观察这个式子,不难发现每个 总共被加了 次。接着推导:

并不能推出 的值,所以要用两个树状数组分别维护 和 的和信息。

那么怎么做区间加呢?考虑给原数组 区间加 给 带来的影响。

因为差分是 ,

  • 多了 而 不变,所以 的值多了 。
  • 不变而 多了 ,所以 的值少了 。
  • 对于不等于 且不等于 的任意 , 和 要么都没发生变化,要么都加了 , 还是 ,所以其它的 均不变。

那就不难想到维护方式了:对于维护 的树状数组,对 单点加 , 单点加 ;对于维护 的树状数组,对 单点加 , 单点加 。

而更弱的问题,「区间加求单点值」,只需用树状数组维护一个差分数组 。询问 的单点值,直接求 的和即可。

这里直接给出「区间加区间和」的代码:

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int t1[MAXN], t2[MAXN], n;

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

void add(int k, int v) {
  int v1 = k * v;
  while (k <= n) {
    t1[k] += v, t2[k] += v1;
    // 注意不能写成 t2[k] += k * v,因为 k 的值已经不是原数组的下标了
    k += lowbit(k);
  }
}

int getsum(int *t, int k) {
  int ret = 0;
  while (k) {
    ret += t[k];
    k -= lowbit(k);
  }
  return ret;
}

void add1(int l, int r, int v) {
  add(l, v), add(r + 1, -v);  // 将区间加差分为两个前缀加
}

long long getsum1(int l, int r) {
  return (r + 1ll) * getsum(t1, r) - 1ll * l * getsum(t1, l - 1) -
         (getsum(t2, r) - getsum(t2, l - 1));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
t1 = [0] * MAXN, t2 = [0] * MAXN; n = 0

def lowbit(x):
    return x & (-x)

def add(k, v):
    v1 = k * v
    while k <= n:
        t1[k] = t1[k] + v; t2[k] = t2[k] + v1
        k = k + lowbit(k)

def getsum(t, k):
    ret = 0
    while k:
        ret = ret + t[k]
        k = k - lowbit(k)
    return ret

def add1(l, r, v):
    add(l, v)
    add(r + 1, -v)

def getsum1(l, r):
    return (r) * getsum(t1, r) - l * getsum(t1, l - 1) - \
          (getsum(t2, r) - getsum(t2, l - 1))

根据这个原理,应该可以实现「区间乘区间积」,「区间异或一个数,求区间异或值」等,只要满足维护的信息和区间操作是同种运算即可,感兴趣的读者可以自己尝试。

二维树状数组

单点修改,子矩阵查询

二维树状数组,也被称作树状数组套树状数组,用来维护二维数组上的单点修改和前缀信息问题。

与一维树状数组类似,我们用 表示 的矩阵总信息,即一个以 为右下角,高 ,宽 的矩阵的总信息。

对于单点修改,设:

即 为 在树状数组树形态上的第 级祖先(第 级祖先是自己)。

则只有 中的元素管辖 ,修改 时只需修改所有 ,其中 ,。

正确性证明

管辖 ,求 和 的取值范围。

考虑一个大小为 的一维树状数组 (对应原数组 )和一个大小为 的一维树状数组 (对应原数组 )。

则命题等价为: 管辖 且 管辖 的条件。

也就是说,在树状数组树形态上, 是 及其祖先中的一个点, 是 及其祖先中的一个点。

所以 ,。

对于查询,我们设:

则合并所有 ,其中 。

正确性证明

设 表示合并两个信息的运算符(比如,如果信息是区间和,则 )。

考虑一个一维树状数组 , 恰好表示原数组上 这段区间信息。

类似地,设 ,则 恰好表示 这个矩阵信息。

又类似地,就有 表示 这个矩阵信息。

其实这里 这个函数如果看成一个树状数组,相当于一个树状数组套了一个树状数组,这也就是「树状数组套树状数组」这个名字的来源。

下面给出单点加、查询子矩阵和的代码。

实现
1
2
3
4
5
6
7
8
void add(int x, int y, int v) {
  for (int i = x; i <= n; i += lowbit(i)) {
    for (int j = y; j <= m; j += lowbit(j)) {
      // 注意这里必须得建循环变量,不能像一维数组一样直接 while (x <= n) 了
      c[i][j] += v;
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int sum(int x, int y) {
  int res = 0;
  for (int i = x; i > 0; i -= lowbit(i)) {
    for (int j = y; j > 0; j -= lowbit(j)) {
      res += c[i][j];
    }
  }
  return res;
}

int ask(int x1, int y1, int x2, int y2) {
  // 查询子矩阵和
  return sum(x2, y2) - sum(x2, y1 - 1) - sum(x1 - 1, y2) + sum(x1 - 1, y1 - 1);
}

子矩阵加,求子矩阵和

前置知识:前缀和 & 差分 和本页面 区间加区间和 一节。

和一维树状数组的「区间加区间和」问题类似,考虑维护差分数组。

二维数组上的差分数组是这样的:

。
为什么这么定义?

这是因为,理想规定状态下,在差分矩阵上做二维前缀和应该得到原矩阵,因为这是一对逆运算。

二维前缀和的公式是这样的:

。

所以,设 是原数组, 是差分数组,有:

移项就得到二维差分的公式了。

。

这样以来,对左上角 ,右下角 的子矩阵区间加 ,相当于在差分数组上,对 和 分别单点加 ,对 和 分别单点加 。

至于原因,把这四个 分别用定义式表示出来,分析一下每项的变化即可。

举个例子吧,初始差分数组为 ,给 子矩阵加 后差分数组会变为:

(其中 这个子矩阵恰好是上面位于中心的 大小的矩阵。)

因此,子矩阵加的做法是:转化为差分数组上的四个单点加操作。

现在考虑查询子矩阵和:

对于点 ,它的二维前缀和可以表示为:

原因就是差分的前缀和的前缀和就是原本的前缀和。

和一维树状数组的「区间加区间和」问题类似,统计 的出现次数,为 。

然后接着推导:

所以我们需维护四个树状数组,分别维护 ,,, 的和信息。

当然了,和一维同理,如果只需要子矩阵加求单点值,维护一个差分数组然后询问前缀和就足够了。

下面给出代码:

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef long long ll;
ll t1[N][N], t2[N][N], t3[N][N], t4[N][N];

void add(ll x, ll y, ll z) {
  for (int X = x; X <= n; X += lowbit(X))
    for (int Y = y; Y <= m; Y += lowbit(Y)) {
      t1[X][Y] += z;
      t2[X][Y] += z * x;  // 注意是 z * x 而不是 z * X,后面同理
      t3[X][Y] += z * y;
      t4[X][Y] += z * x * y;
    }
}

void range_add(ll xa, ll ya, ll xb, ll yb,
               ll z) {  //(xa, ya) 到 (xb, yb) 子矩阵
  add(xa, ya, z);
  add(xa, yb + 1, -z);
  add(xb + 1, ya, -z);
  add(xb + 1, yb + 1, z);
}

ll ask(ll x, ll y) {
  ll res = 0;
  for (int i = x; i; i -= lowbit(i))
    for (int j = y; j; j -= lowbit(j))
      res += (x + 1) * (y + 1) * t1[i][j] - (y + 1) * t2[i][j] -
             (x + 1) * t3[i][j] + t4[i][j];
  return res;
}

ll range_ask(ll xa, ll ya, ll xb, ll yb) {
  return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1);
}

权值树状数组及应用

我们知道,普通树状数组直接在原序列的基础上构建, 表示的就是 的区间信息。

然而事实上,我们还可以在原序列的权值数组上构建树状数组,这就是权值树状数组。

什么是权值数组?

一个序列 的权值数组 ,满足 的值为 在 中的出现次数。

例如: 的权值数组为 。

很明显, 的大小和 的值域有关。

若原数列值域过大,且重要的不是具体值而是值与值之间的相对大小关系,常 离散化 原数组后再建立权值数组。

另外,权值数组是原数组无序性的一种表示:它重点描述数组的元素内容,忽略了数组的顺序,若两数组只是顺序不同,所含内容一致,则它们的权值数组相同。

因此,对于给定数组的顺序不影响答案的问题,在权值数组的基础上思考一般更直观,比如 [NOIP2021] 数列。

运用权值树状数组,我们可以解决一些经典问题。

单点修改,查询全局第 小

在此处只讨论第 小,第 大问题可以通过简单计算转化为第 小问题。

该问题可离散化,如果原序列 值域过大,离散化后再建立权值数组 。注意,还要把单点修改中的涉及到的值也一起离散化,不能只离散化原数组 中的元素。

对于单点修改,只需将对原数列的单点修改转化为对权值数组的单点修改即可。具体来说,原数组 从 修改为 ,转化为对权值数组 的单点修改就是 单点减 , 单点加 。

对于查询第 小,考虑二分 ,查询权值数组中 的前缀和,找到 使得 的前缀和 而 的前缀和 ,则第 大的数是 (注:这里认为 的前缀和是 )。

这样做时间复杂度是 的。

考虑用倍增替代二分。

设 ,,枚举 从 降为 :

  • 查询权值数组中 的区间和 。
  • 如果 ,扩展成功,,;否则扩展失败,不操作。

这样得到的 是满足 前缀和 的最大值,所以最终 就是答案。

看起来这种方法时间效率没有任何改善,但事实上,查询 的区间和只需访问 的值即可。

原因很简单,考虑 ,它一定是 ,因为 之前只累加过 满足 。因此 表示的区间就是 。

如此以来,时间复杂度降低为 。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 权值树状数组查询第 k 小
int kth(int k) {
  int sum = 0, x = 0;
  for (int i = log2(n); ~i; --i) {
    x += 1 << i;                    // 尝试扩展
    if (x >= n || sum + t[x] >= k)  // 如果扩展失败
      x -= 1 << i;
    else
      sum += t[x];
  }
  return x + 1;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 权值树状数组查询第 k 小
def kth(k):
    sum = 0; x = 0
    i = log2(n)
    while ~i:
        x = x + (1 << i) # 尝试扩展
        if x >= n or sum + t[x] >= k: # 如果扩展失败
            x = x - (1 << i)
        else:
            sum = sum + t[x]
    return x + 1

全局逆序对(全局二维偏序)

全局逆序对也可以用权值树状数组巧妙解决。问题是这样的:给定长度为 的序列 ,求 中满足 且 的数对 的数量。

该问题可离散化,如果原序列 值域过大,离散化后再建立权值数组 。

我们考虑从 到 倒序枚举 ,作为逆序对中第一个元素的索引,然后计算有多少个 满足 ,最后累计答案即可。

事实上,我们只需要这样做(设当前 ):

  • 查询 的前缀和,即为左端点为 的逆序对数量。
  • 自增 ;

原因十分自然:出现在 中的元素一定比当前的 小,而 的倒序枚举,自然使得这些已在权值数组中的元素,在原数组上的索引 大于当前遍历到的索引 。

用例子说明,。

按照 扫:

  • ,查询 前缀和,为 , 自增 ,。
  • ,查询 前缀和,为 , 自增 ,。
  • ,查询 前缀和,为 , 自增 ,。
  • ,查询 前缀和,为 , 自增 ,。
  • ,查询 前缀和,为 , 自增 ,。

所以最终答案为 。

注意到,遍历 后的查询 和自增 的两个步骤可以颠倒,变成先自增 再查询 ,不影响答案。两个角度来解释:

  • 对 的修改不影响对 的查询。
  • 颠倒后,实质是在查询 且 的数对数量,而 时不存在 ,所以 相当于 ,所以这与原来的逆序对问题是等价的。

如果查询非严格逆序对( 且 )的数量,那就要改为查询 的和,这时就不能颠倒两步了,还是两个角度来解释:

  • 对 的修改 影响 对 的查询。
  • 颠倒后,实质是在查询 且 的数对数量,而 时恒有 ,所以 不相当于 ,与原问题 不等价。

如果查询 且 的数对数量,那这两步就需要颠倒了。

另外,对于原逆序对问题,还有一种做法是正着枚举 ,查询有多少 满足 。做法如下(设 ):

  • 查询 ( 是 的大小,即 的值域(或离散化后的值域))的区间和。
  • 将 自增 。

原因:出现在 中的元素一定比当前的 大,而 的正序枚举,自然使得这些已在权值数组中的元素,在原数组上的索引 小于当前遍历到的索引 。

树状数组维护不可差分信息

比如维护区间最值等。

注意,这种方法虽然码量小,但单点修改和区间查询的时间复杂度均为 ,比使用线段树的时间复杂度 劣。

区间查询

我们还是基于之前的思路,从 沿着 一直向前跳,但是我们不能跳到 的左边。

因此,如果我们跳到了 ,先判断下一次要跳到的 是否小于 :

  • 如果小于 ,我们直接把 单点 合并到总信息里,然后跳到 。
  • 如果大于等于 ,说明没越界,正常合并 ,然后跳到 即可。

下面以查询区间最大值为例,给出代码:

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int getmax(int l, int r) {
  int ans = 0;
  while (r >= l) {
    ans = max(ans, a[r]);
    --r;
    for (; r - lowbit(r) >= l; r -= lowbit(r)) {
      // 注意,循环条件不要写成 r - lowbit(r) + 1 >= l
      // 否则 l = 1 时,r 跳到 0 会死循环
      ans = max(ans, C[r]);
    }
  }
  return ans;
}

可以证明,上述算法的时间复杂度是 。

时间复杂度证明

考虑 和 不同的最高位,一定有 在这一位上为 , 在这一位上为 (因为 )。

如果 在这一位的后面仍然有 ,一定有 ,所以下一步一定是把 的最低位 填为 ;

如果 的这一位 就是 的最低位 ,无论是 还是 , 的这一位 一定会变为 。

因此, 经过至多 次变换后, 和 不同的最高位一定可以下降一位。所以,总时间复杂度是 。

单点更新

注

请先理解树状数组树形态的以下两条性质,再学习本节。

  • 设 ,则其儿子数量为 ,编号分别为 。
  • 的所有儿子对应 的管辖区间恰好拼接成 。

关于这两条性质的含义及证明,都可以在本页面的 树状数组与其树形态的性质 一节找到。

更新 后,我们只需要更新满足在树状数组树形态上,满足 是 的祖先的 。

对于最值(以最大值为例),一种常见的错误想法是,如果 修改成 ,则将所有 更新为 。下面是一个反例: 中将 修改成 ,最大值是 ,但按照上面的修改这样会得到 。将 直接修改为 也是错误的,一个反例是,将上面例子中的 修改为 。

事实上,对于不可差分信息,不存在通过 直接修改 的方式。这是因为修改本身就相当于是把旧数从原区间「移除」,然后加入一个新数。「移除」时对区间信息的影响,相当于做「逆运算」,而不可差分信息不存在「逆运算」,所以无法直接修改 。

换句话说,对每个受影响的 ,这个区间的信息我们必定要重构了。

考虑 的儿子们,它们的信息一定是正确的(因为我们先更新儿子再更新父亲),而这些儿子又恰好组成了 这一段管辖区间,那再合并一个单点 就可以合并出 ,也就是 了。这样,我们能用至多 个区间重构合并出每个需要修改的 。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void update(int x, int v) {
  a[x] = v;
  for (int i = x; i <= n; i += lowbit(i)) {
    // 枚举受影响的区间
    C[i] = a[i];
    for (int j = 1; j < lowbit(i); j *= 2) {
      C[i] = max(C[i], C[i - j]);
    }
  }
}

容易看出上述算法时间复杂度为 。

建树

可以考虑拆成 个单点修改, 建树。

也有 的建树方法,见本页面 建树 一节的方法一。

Tricks

建树

以维护区间和为例。

方法一:

每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新自己的直接父亲。

实现
1
2
3
4
5
6
7
8
// Θ(n) 建树
void init() {
  for (int i = 1; i <= n; ++i) {
    t[i] += a[i];
    int j = i + lowbit(i);
    if (j <= n) t[j] += t[i];
  }
}
1
2
3
4
5
6
7
# Θ(n) 建树
def init():
    for i in range(1, n + 1):
        t[i] = t[i] + a[i]
        j = i + lowbit(i)
        if j <= n:
            t[j] = t[j] + t[i]

方法二:

前面讲到 表示的区间是 ,那么我们可以先预处理一个 前缀和数组,再计算 数组。

实现
1
2
3
4
5
6
// Θ(n) 建树
void init() {
  for (int i = 1; i <= n; ++i) {
    t[i] = sum[i] - sum[i - lowbit(i)];
  }
}
1
2
3
4
# Θ(n) 建树
def init():
    for i in range(1, n + 1):
        t[i] = sum[i] - sum[i-lowbit(i)]

时间戳优化

对付多组数据很常见的技巧。若每次输入新数据都暴力清空树状数组,就可能会造成超时。因此使用 标记,存储当前节点上次使用时间(即最近一次是被第几组数据使用)。每次操作时判断这个位置 中的时间和当前时间是否相同,就可以判断这个位置应该是 还是数组内的值。

实现
1
2
// 时间戳优化
int tag[MAXN], t[MAXN], Tag;

void reset() { ++Tag; }

void add(int k, int v) { while (k <= n) { if (tag[k] != Tag) t[k] = 0; t[k] += v, tag[k] = Tag; k += lowbit(k); } }

int getsum(int k) { int ret = 0; while (k) { if (tag[k] == Tag) ret += t[k]; k -= lowbit(k); } return ret; } ```

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 时间戳优化
tag = [0] * MAXN; t = [0] * MAXN; Tag = 0
def reset():
    Tag = Tag + 1
def add(k, v):
    while k <= n:
        if tag[k] != Tag:
            t[k] = 0
        t[k] = t[k] + v
        tag[k] = Tag
        k = k + lowbit(k)
def getsum(k):
    ret = 0
    while k:
        if tag[k] == Tag:
            ret = ret + t[k]
        k = k - lowbit(k)
    return ret

例题