树状数组

树状数组

树状数组定义

  • 存在一个原数组 \(f\)

  • 存在一个 \(int\) 型数组 \(tree\)

  • 定义 \(tree[x]\) 为以位置 \(x\) 为结尾,长度为 \(lowbit(x)\) 的一串数的和

树状数组原理

首先,我们先给定一个整数: \(num\) = 43

我们将 \(num\) 按二进制进行划分

43 = \((101011)_2\)

43 = \((100000)_2\) + \((1000)_2\) + \((10)_2\) + \((1)_2\)

我们可以发现: 将这一个整数拆分为 \(二\) 进制的时间复杂度为 \(log_2 n\)

现在我们来想,假设给定一个长度为 43 的数组 \(f\) , 我们如何将这一个数组的和, 借用二进制的特点来表示出来

于是我们发现一种拆分方法

image

我们将原数组 \(f\) 的前缀和数组命名为 \(c\) , 我们可以发现 \(c[43]\) = \(tree[43]\) + \(tree[42]\) + \(tree[40]\) + \(tree[32]\)

  • \(tree[43]\) = \(f[43]\) \(lowbit(43)\) = 1
  • \(tree[42]\) = \(f[42]\) + \(f[41]\) \(lowbit(42)\) = 2
  • \(tree[40]\) = \(f[40]\) + ... + \(f[33]\) \(lowbit(40)\) = 8
  • \(tree[32]\) = \(f[32]\) + ... + \(f[1]\) \(lowbit(32)\) = 32

通过这种方式,我们发现可以以 \(log_2n\) 的时间复杂度将区间内任意一段子序列表示出来,其中 \(tree\) 数组存储的就是以位置 \(x\) 为结尾,长度为 \(lowbit(x)\) 的一串数的和,\(tree\) 数组与 \(a\) 数组也就被称为树状数组

那么为什么 \(tree\) 数组与 \(a\) 数组被称为树状数组呢?

我们来看下面这张图,下图的 \(C\) 数组就是我们的 \(tree\) 数组, \(a\) 数组就是 \(f\) 数组

image

我们可以发现 \(C\) 数组与 \(a\) 数组构成了一棵类似树的结构,每一个 \(C[x]\) 就是其子节点的和

例如:

\(C[8]\) = \(C[4]\) + \(C[6]\) + \(C[7]\) + \(a[8]\)

前 8 个 数的和 = \(C[8]\)

\(C[12]\) = \(C[10]\) + \(C[11]\) + \(a[12]\)

前 12 个 数的和 = \(C[8]\) + \(C[12]\)

那么我们来想一下另一个问题,给定一个子节点 \(q\) , 我们怎么找到他的父节点

我们先假设 \(q\) 最后一个 1 后面有着 x 个 0, 所以 \(q\) 所表示的范围就是 \([q - 2 ^ x + 1, q]\)

现在我们继续假设一个 \(h\) , \(h\)\(q\) 与他父节点的距离,即 父节点 = \(h\) + \(q\) ,设 \(u\)\(h + q\) 最后一个 1 后面 0 的个数

我们来想一下当 \(h \lt 2^x\) 时,父节点所表示的范围就是 \([q + h - 2^u + 1, q + h]\) ,因为 \(h \lt 2^x\) ,所以 \(u \lt x\) 并且 \(2^u \le h\) ,所以 \(q + h - 2^u + 1 \ge q + 1\) ,所以 \(h + q\) 这个点不可能是 \(q\) 这个点的父节点

\(h = 2^x\) 时,这时候 \(u \ge x + 1\) ,那么 \(q + h - 2^u + 1 \le q - 2 ^ x + 1\) ,由此,我们就可以证出节点 \(q\) 的父节点就是 \(q\) + \(lowbit(q)\),这一点,也完全符合表中的规律

树状数组操作

现在有这么一道题目:

给定一个长度为 \(n\) 的数组 \(f\) ,给定 \(q\) 次操作,每一次操作可能有两种情况,一是给定 \(x\)\(z\) ,将 \(f\) 数组位置为 \(x\) 的数加上 \(z\) , 二是给定 \(l\)\(r\) ,要求你求出原数组该区间的总和。

我们来考虑一下这道题,如果我们暴力来做,操作一的时间复杂度为 \(O(1)\) ,操作二的时间复杂度为 \(O(n)\) , 如果 \(q\)\(n\) 的范围均是 100000,那么总时间复杂度就是 \(O(n \times q)\) ,显而易见会超时,或者我们使用前面所学过的前缀和知识,这样的话操作二的时间复杂度被优化为 \(O(1)\) , 但操作一每一次加上需要从位置 \(x\) 向后更新一边前缀和数组,总的时间复杂度依旧是 \(O(n \times q)\) ,显而易见,也会超时

现在我们来考虑一下是否能够利用树状数组来优化

首先,我们先来看操作二,我们可以以 \(log_2 n\) 的时间复杂度分别求出 \(l - 1\)\(r\) 的前缀和

之后,我们考虑操作一:

我们思考一下,我们修改原数组中位置为 \(x\) 的元素,在 \(tree\) 数组中要修改更新多少,也就是找 \(tree\) 数组中有多少元素是覆盖原数组位置为 \(x\) 的元素的,也就是 \(x\) 的所有祖先节点,也就是 x 一直加 lowbit(x),一直到 \(n\) ,我们发现这一操作最多执行 \(log_2 n\) 次,时间复杂度为 \(O(log_2n)\)

总时间复杂度为 \(O(q \times log_2n)\) 不会超时

我们也可以看出树状数组的作用,树状数组是用来动态维护前缀结果的

树状数组的优缺点

树状数组(BIT)的优点

  • 代码精炼,实现轻松。
  • queryupdate 操作时间复杂度都只需要 \(O(log_2n)\)
  • 算法常数小,相比于线段树更快

树状数组(BIT)的缺点

  • 应用场景有限:较为复杂的区间操作无法实现,只能使用线段树

树状数组的应用

单点修改,区间查询

题目:【模板】树状数组 1

代码:

#include <bits/stdc++.h>

using namespace std;

#define int long long

#define dbg(x) cout << #x << " = " << x << endl

const int N = 1e6 + 15;
int n, m, t[N];

void update(int x, int w) {
    for (int i = x; i <= n; i += i & -i)
        t[i] += w;
}

int query(int x) {
    int sum = 0;
    for (int i = x; i; i -= i & -i)
        sum += t[i];
    return sum;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;

    for (int i = 1; i <= n; i++) {
        int x; cin >> x;
        update(i, x);
    }

    while (m--) {
        int op; cin >> op;
        int a, b; cin >> a >> b;
        if (op == 1)
            update(a, b);
        else
            cout << query(b) - query(a - 1) << '\n';
    }
} 

单点查询 区间修改

题目:【模板】树状数组 2

代码:

#include <bits/stdc++.h>

using namespace std;

#define int long long

#define dbg(x) cout << #x << " = " << x << endl

const int N = 1e6 + 15;
int n, m, t[N], f[N];

void update(int x, int w) {
    for (int i = x; i <= n; i += i & -i)
        t[i] += w;
}

int query(int x) {
    int sum = 0;
    for (int i = x; i; i -= i & -i)
        sum += t[i];
    return sum;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;

    for (int i = 1; i <= n; i++)
        cin >> f[i];

    while (m--) {
        int op; cin >> op;

        if (op == 1) {
            int x, y, k;
            cin >> x >> y >> k;
            update(x, k);
            update(y + 1, -k);
        }
        else {
            int x; cin >> x;
            cout << f[x] + query(x) << '\n';
        }
    }
} 

例题

image

例题代码

posted @ 2025-02-19 17:20  APF  阅读(48)  评论(0)    收藏  举报