「观前提醒」
「文章仅供学习和参考,如有问题请在评论区提出」
前言#
这也算是我写正儿八经的博客,因为没怎么写过,所以可能有些地方没讲好或者有点啰嗦。但是我也会尽可能地解释明白其中的具体实现方法。要是有什么错误和问题,欢迎在评论区里指正和提问。
定义#
树状数组是一种支持 单点修改 和 区间查询 的数据结构。
普通的树状数组只能用来维护像加法、乘法、异或等,这样满足结合律和可差分的信息。
- 结合律:
,其中 是一个二元运算符。 - 可差分:具有逆运算的运算,即已知
和 可以求出 。
基本概念#
对于普通的数组来存储数据,修改一个数的时间复杂度是
所以对于这种单点修改和区间查询都有的操作,这两种数据结构都不能很好地完成任务。而树状数组就用了一种折中的法子来应对两种操作都有的情况,利用二进制的原理来维护前缀和,从而达到:
- 时间复杂度
- 单点修改:
- 区间查询:
- 单点修改:
- 空间复杂度:
基本原理#
树状数组是根据二进制的原理对数据进行存储,基本存储逻辑如下图所示。
这样的话,我们在查询前缀和的时候就只需要把相对应的区间加和就行了。
例如,当我们要获取
时的前缀和 时,就只需要把 里的 和就行,即 。
那么对于
根据二进制原理,对于一个正整数
代表 的二进制表示中,从低位到高位,第 个 出现的位次(从 开始)。 例如:
,那么 。
那么我们发现,
例如,
,最低位 所对应的值为 ,所以 。
单点修改#
分析#
从上面我们知道树状数组
假设现在要给
因为
那么问题来了,该怎么寻找
这里先给出结论,对于
一种思路是,我们可以观察存储的逻辑图来找规律。我们发现
另一种思路就是,我们可以通过二进制来看。通过上图我们知道,
同样我们也能得出结论,
这样每次通过 x += lowbit(x);
来更新数值,直到数组结尾
lowbit(x) 解释(会的话可以直接跳过)
功能:可以用来求一个非负整数数
最低位 所代表的数值。 原理:它是利用了负数的补码特性。
我们知道,一个负数
的二进制表示,是在其对应正数 的二进制表示进行取反加一 得来的。 例如,43 的二进制表示为 ...101011 // 前面省略 0 按位取反 ...010100 // 前面省略 1 + 1 ...010101 // 前面省略 1 最后的出来的(...010101) 就是 -43 的二进制表示
是将 和 进行按位与(&)操作,然后得到 最低位 所代表的数。 代码实现
void lowbit(x) { return x & -x; }
如果实在不明白,可以先去学一学二进制及位运算相关的知识。
代码实现#
const int N = 1e5 + 10;
int tr[N]; // tr[] 存储树状数组数据
int a[N]; // a[] 存储原数组数据
int n; // 数组的长度
// 返回非负整数 x 在二进制表示下最低位 1 及其后面 0 构成的数值
int lowbit(int x) { return x & -x; }
// 将序列中第 x 个数加上 c
void add(int x, int c) {
// 不断寻找自己的父亲节点,然后加上增减的值
for (int i = x; i <= n; i += lowbit(i))
tr[i] += c;
}
// 使用举例
add(2, 3); // 将序列中下标为 2 的数加上 3
add(5, -7); // 将序列中下标为 5 的数减去 7
// 这里的每次操作都是对 tr[]进行修改, 而不会对 a[] 产生影响。
区间查询#
分析#
所谓区间查询,也就是利用前缀和
前面我们知道了树状数组每个
然后拿出我们的万用图进行找规律(一招鲜,吃遍天)
我们能够发现,对于
然后我们就能发现,这其实就是获取
那么这样的话就也需要用到
这样每次通过 x -= lowbit(x);
来更新数值,因为是对
代码实现#
const int N = 1e5 + 10;
int tr[N]; // tr[] 存储树状数组数据
int a[N]; // a[] 存储原数组数据
// 返回非负整数 x 在二进制表示下最低位 1 及其后面 0 构成的数值
int lowbit(int x) { return x & -x; }
// 查询序列前 x 个数的和
int query(int x) {
int res = 0;
// 不断进行回退, 直到 tr[0]
for (int i = x; i; i -= lowbit(i))
res += tr[i];
return res;
}
// 操作举例
int sum1 = query(x); // 查询 a[1] ~ a[x]的和
int sum2 = query(r) - query(l - 1); // 查询 a[l] ~ a[r] 的和
整体代码#
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int tr[N]; // tr[] 存储树状数组数据
int a[N]; // a[] 存储原数组数据
// 返回非负整数 x 在二进制表示下最低位 1 及其后面 0 构成的数值
int lowbit(int x) { return x & -x; }
// 将序列中第 x 个数加上 c
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i))
tr[i] += c;
}
// 查询序列前 x 个数的和
void query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i))
res += tr[i];
return res;
}
int main() {
int n;
cin >> n;
// 获取原数组数据
for (int i = 1; i <= n; i++) cin >> a[i];
// 建立树状数组
for (int i = 1; i <= n; i++)
add(i, a[i]); // 在下标为 i 的位置加上 a[i]
add(3, 20); // 在下标为 3 的位置加上 20
add(4, -30);// 在下标为 4 的位置减去 30
add(15 - query(2)); // 将下标为 2 的位置的值改为 15
int sum1 = query(11); // 获取序列前 11 个数的和
int sum2 = query(14, 3); // 获取序列下标 3~14 的和
return 0;
}
练手题目#
小结#
树状数组巧妙地运用了二进制的存储逻辑,使得能够在
而且这里只是讲了树状数组最基本的应用,上面的例题也都是模板题。至于树状数组的拓展使用,之后应该会再写的。。。。
参考资料#
树状数组 – OI Wiki (oi-wiki.org):https://oi-wiki.org/ds/fenwick/