在前面「前缀与差分」的章节中,我们学习了利用数组,把每次区间操作的时间复杂度将至 5.11.3 树状数组 * - 图1#card=math&code=O%281%29&id=U1jyf) 的方法。然而,这些方法直到做完所有更新操作,还原出原数组后,才能在原数组上查询。树状数组专为解决区间改查问题而生。

5.11.3.1 单点修改,区间查询

树形结构使得这一数据结构可以在对数阶的时间内搜索到任意节点。进一步理解树状数组,我们提供了参考资料的推荐,它们讲解了普通树状数组后,也会讲解更为复杂的应用。你可以先学习本章节列出的部分。

  1. // 普通树状数组
  2. #include <bits/stdc++.h>
  3. using namespace std;
  4. const int N = 10004;
  5. int c[N];
  6. int n;
  7. int lowbit(int x) {
  8. return x & -x;
  9. }
  10. void update(int x, int u) {
  11. for (int i = x; i <= n; i += lowbit(i)) {
  12. c[i] += u;
  13. }
  14. }
  15. // sum[1, x]
  16. void query(int x) {
  17. int res = 0;
  18. for (int i = x; i > 0; i -= lowbit(i)) {
  19. res += c[i];
  20. }
  21. return res;
  22. }
  23. // sum[l, r]
  24. int rangeQuery(int l, int r) {
  25. return query(r) - query(l - 1);
  26. }
  27. int main() {
  28. return 0;
  29. }

操作时间复杂度:5.11.3 树状数组 * - 图2#card=math&code=O%28%5Clog%20n%29&id=CoHBK)

参考:

树状数组详解

树状数组

5.11.3.2 区间修改,区间查询

我们知道,差分数组支持用单点修改表示区间修改。差分树状数组就是在普通树状数组上应用了差分的原理。这一模板需要两个数组。

  1. // 差分树状数组
  2. #include <bits/stdc++.h>
  3. using namespace std;
  4. const int N = 10004;
  5. int ca[N]; // ca[i] = a[i] - a[i - 1]
  6. int cb[N]; // cb[i] = ca[i] * (i - 1)
  7. int n;
  8. int lowbit(int x) {
  9. return x & -x;
  10. }
  11. void update(int x, int u) {
  12. for (int i = x; i <= n; i += lowbit(i)) {
  13. ca[i] += u;
  14. cb[i] += u * x; // 如果这里写成 cb[i] += u * (x - 1)
  15. }
  16. }
  17. // sum[1, x]
  18. int query(int x) {
  19. int res = 0;
  20. for (int i = x; i > 0; i -= lowbit(i)) {
  21. res += (x + 1) * ca[i] - cb[i]; // 这里就写成 res += x * ..
  22. }
  23. return res;
  24. }
  25. // sum[l, r]
  26. int rangeQuery(int l, int r) {
  27. return query(r) - query(l - 1);
  28. }
  29. int main() {
  30. return 0;
  31. }

5.11.3.3 树状数组求逆序对

例题:牛客 15163. 逆序数

在前面,我们使用了归并排序求逆序对,树状数组也可以很清晰地解决求逆序对问题。它的思路是,每插入一个数时,统计树状数组中大于它的数,加入答案。

参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100004;
int c[N];
int n;
int lowbit(int x) {
    return x & -x;
}
void update(int x, int u) {
    for (; x <= n; x += lowbit(x)) {
        c[x] += u;
    }
}
int query(int x) {
    int res = 0;
    for (; x > 0; x -= lowbit(x)) {
        res += c[x];
    }
    return res;
}
int rangeQuery(int l, int r) {
    return query(r) - query(l - 1);
}
int main() {
    cin >> n;
    long long ans = 0;
    for (int i=0; i<n; ++i) {
        int x;
        cin >> x;
        ans += rangeQuery(x + 1, n);
        update(x, 1);
    }
    cout << ans << endl;
    return 0;
}

在上面的代码中,模板在 for 循环中省略了循环变量 i ,这也是正确的。也有人在这里写成 while 循环,这与个人习惯有关。你可以根据自己的情况,修改出最符合自己习惯和理解的模板。

然而这一求逆序对方法的弊端也很显然——那就是额外空间。归并排序只需要数的个数数量的额外空间;而树状数组需要数的范围数量的额外空间。在数的范围大、分布稀疏时,需要「离散化」操作,把大数映射到小数。

推荐阅读:树状数组和逆序对