树状数组是用来实现单点查询,区间修改的数据结构,虽然功能上可以被线段树完全覆盖,但是容易实现.
P3374 【模板】树状数组 1
基础树状数组#
构造#
我们让树状数组 c 的每一项 c[i] 都存储以 i 为最终位置,以 lowbit(i) 为长度的区间中所有数的和,即 c[i] 存储区间 .
什么是 lowbit
lowbitlowbit,即低位,是指一个数的二进制从右起的第一个 1 和这个 1 右边的所有 0 所构成的新的二进制数.或者说是最右边的 1 所对应的权值.
如,
例如,一个 16 个元素的数组,其树状数组的管辖区间示意如下:(摘自 oi-wiki ↗)

如果写成二进制,更直观地:
令 的二进制表示中,从右起第一个 1 位于第 位(即 ),则 管辖的区间由其本身 和所有满足以下条件的正整数 组成:
的二进制中,第 位及比 更高的所有位与 对应相等,低于第 位的部分可以是任意值.
例:对于 ,二进制为 101 1000,从右起第一个 1 在第 4 位.
因此 存储的数为所有形如 101 xxxx 但实际数值不超过 的正整数——即固定高三位 101,低四位从 0000 变到 1000,对应区间 .
根据二进制知识可知,lowbit(i) = i & -i.
为什么?
由于 -i 在计算机里是补码,等于 ~i + 1(按位取反再加 1).
这样操作后,若记 i 最右边的一个 1 为 ,由于取反, 右边的所有位都会变成 1,在加一后由于进位,再次变成 0. 在取反时变成 0,但由于进位变回 1. 左边的所有位都由于取反和原来不一样.这样,在按位与后,除了 必然为 1 之外,其余的位必然都为 0.
例子:
i = 6 → 二进制 110,-i 是 010(补码计算:~110 = 001,加 1 得 010).
110 & 010 = 010 → 正是最右边的 1(二进制 10,即 2).
查询操作#
想要对区间 查询,只需要知道区间 和区间 的和,之后用后者减去前者即可.
如何进行前缀查询呢?
例如,我想要查询区间 ,先看 ,能管辖 ,那 6 及之前的怎么办呢? 肯定包含 的,于是找到 ,能管辖 ,同样的,那 4 及之前的怎么办呢,再看到 ,能管辖 ,不剩了.
再例如,我想要查询区间 ,先看 ,能管辖 ,于是再找到 ,能管辖 ,不剩了,故答案为 .
一般的,我想要查询区间 ,先看 ,发现 所能够管辖到的最小值为 ,在将 加和到 中后,原问题就转化为查询区间 ,边界条件是 .
时间复杂度 .
// 查询区间[1,ed]
int query(int ed) {
int ans = 0;
for (int i = ed; i >= 1; i -= i & -i) {
ans += c[i];
}
return ans;
}
// 查询区间[l,r]
int query(int l, int r) {
return query(r) - query(l - 1);
}cpp修改操作#
我们现在尝试修改序列的第 项.
我们知道,树状数组是一棵树的形态,因此,修改一个元素,只需要更新记录这个元素的节点即可.
通过观察上图,所有被影响的节点会组成一条链的形状,且对于节点 ,最底端(即下标最小)的节点为 .只需要找出 及所有的祖先节点,将它们都同步进行更改即可.
注意到,节点 的父节点总是 .(节点 的父节点为 最小的、真包含 的节点)
因此,得出修改操作的一般步骤:
- 令 初始化为
- 修改
- 将 设定为 ,若 ,回到步骤 2
时间复杂度 .
void add(int t, int k) {
for (int i = t; i <= n; i += i & -i) {
c[i] += k;
}
}cpp建树#
一般来说,建树只需要转化为 次单点增加就可以了.时间复杂度 .
但是有没有 建树的方法呢?
前面提到,,可以使用前缀和优化.
for (int i = 1; i <= n; i++) {
cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
for (int i = 1; i <= n; i++) {
c[i] = sum[i] - sum[i - (i & -i)];
}cpp标程#
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 100;
int n, m, c[N], a[N], sum[N];
class Tree {
public:
int query(int ed) {
int ans = 0;
for (int i = ed; i >= 1; i -= i & -i) {
ans += c[i];
}
return ans;
}
int query(int l, int r) {
return query(r) - query(l - 1);
}
void add(int t, int k) {
for (int i = t; i <= n; i += i & -i) {
c[i] += k;
}
}
};
int main() {
cin >> n >> m;
Tree tree;
for (int i = 1; i <= n; i++) {
cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
for (int i = 1; i <= n; i++) {
c[i] = sum[i] - sum[i - (i & -i)];
}
for (int i = 1; i <= m; i++) {
int op, x, k;
cin >> op >> x >> k;
if (op == 1)
tree.add(x, k);
else
cout << tree.query(x, k) << endl;
}
return 0;
}cpp树状数组变体#
如果需要单点查询,区间修改,又该怎么做呢?
P3368 【模板】树状数组 2
很简单,只要使用差分来维护就可以了.
树状数组的主体不用改变,在建树时,由维护数组值改为维护差分值.
for (int i = 1; i <= n; i++) {
cin >> a[i];
tree.add(i, a[i] - a[i - 1]);
}cpp对于查询第 项,等同于区间查询差分数组 .
cin >> x;
cout << tree.query(1, x) << endl;cpp对于区间修改,设第 到 项增加 ,等同于 增加 , 减少 .
cin >> x >> y >> k;
tree.add(x, k);
tree.add(y + 1, -k);cpp标程#
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 100;
int n, m, c[N], a[N], sum[N];
class Tree {
public:
int query(int ed) {
int ans = 0;
for (int i = ed; i >= 1; i -= i & -i) {
ans += c[i];
}
return ans;
}
int query(int l, int r) {
return query(r) - query(l - 1);
}
void add(int t, int k) {
for (int i = t; i <= n; i += i & -i) {
c[i] += k;
}
}
};
int main() {
cin >> n >> m;
Tree tree;
for (int i = 1; i <= n; i++) {
cin >> a[i];
tree.add(i, a[i] - a[i - 1]);
}
for (int i = 1; i <= m; i++) {
int op, x, y, k;
cin >> op;
if (op == 1) {
cin >> x >> y >> k;
tree.add(x, k);
tree.add(y + 1, -k);
}
else {
cin >> x;
cout << tree.query(1, x) << endl;
}
}
return 0;
}cpp