树状数组
树状数组
树状数组定义
-
存在一个原数组 \(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\) , 我们如何将这一个数组的和, 借用二进制的特点来表示出来
于是我们发现一种拆分方法

我们将原数组 \(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\) 数组

我们可以发现 \(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)的优点
- 代码精炼,实现轻松。
query与update操作时间复杂度都只需要 \(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';
}
}
}
例题


浙公网安备 33010602011771号