树状数组基础
树状数组,是一种可以在 的时间内,实现单点修改和查询前缀和的操作数据结构。
如何实现呢?我们先从查询前缀和开始。
若一个正整数 的二进制形式可以被表示为 ,其中等于 的位有 , 可以被分解为 的形式。
然后,查询一个前缀和就相当于查询区间 的和。
设 ,则区间 可以被划分为 个不重叠的区间:
,
,
,
,
。
()
这些区间的长度分别为 。
设第 个区间的结尾为 ,则第 个区间的长度为 。
树状数组就使用了这种思想。具体来讲,对于一个需要维护的序列 ,建一个序列 , 的值为 。
假如我们要查询 的前缀和。
可以被划分为 个区间:。

如图,我们首先从 开始,,把结果加上 。
我们从 往前跳 步,跳到 ,,把结果加上 。
我们从 往前跳 步,跳到 ,,把结果加上 。
我们从 往前跳 步,跳到 ,发现 已经不被 包含,返回结果。
可以写出查询前缀和的代码:
int query(int p){
int sum = 0;
while (p) sum += s[p], p -= lowbit(x);
return sum;
}
单点增加,就是修改 ,并正确维护 数组。显然,我们只需要将 数组内包含 的项增加。
可以写出单点修改的代码:
void update(int p, int x){
while (p <= n) s[p] += x, p += lowbit(p);
}
模板题一代码(查询 时只需查询 即可):
#include <algorithm>
#include <cstdio>
using namespace std;
int n, m, t[500001], tt, ttt, tttt;
int lowbit(int x){
return x & -x;
}
void add(int i, int x){
while (i <= n){
t[i] += x;
i += lowbit(i);
}
}
int sum(int i){
int s = 0;
while (i > 0){
s += t[i];
i -= lowbit(i);
}
return s;
}
int main(){
scanf("%d%d", &n, &m);
for (int i = 1;i <= n;i ++) scanf("%d", &tt), add(i, tt);
for (int i = 1;i <= m;i ++){
scanf("%d%d%d", &tt, &ttt, &tttt);
if (tt == 1) add(ttt, tttt);
else printf("%d\n", sum(tttt) - sum(ttt - 1));
}
}
树状数组的技巧
值域树状数组
我们可以将树状数组作为一个桶来使用。
具体的讲,就是每一次新增一个值 时,执行 update(x, 1)
的操作;每一次删除一个值 时,执行 update(x, -1)
的操作。利用树状数组的查询前缀和功能,我们可以快速回答,有多少个数的值域在 中这样的问题。
当值域过大时,我们可以使用离散化来将数映射到 的范围内,再进行下一步操作。
树状数组上倍增
当在使用树状数组的时候,我们经常会应对一些答案有单调性的问题(比如在值域树状数组上寻找第 小)。
一种显然的解决方案就是使用二分,但是会增加一个 的时间复杂度,时间复杂度为 。
有没有一种不增加时间复杂度的解决方案呢?当然是有的。
由于树状数组的每一个节点存储的都是一个长度为 的整次幂的区间和,我们可以使用同样思想的倍增算法。
我们以值域树状数组上寻找第 小为例。
我们设两个变量 与 ,都初始化为 。
然后,我们可以从 倒序枚举 ,对于每一个 ,如果 且 ,,。
最后, 即为第 小的值。由于倍增与树状数组相结合,可以直接在 数组内查询,免去了调用查询操作的 ,时间复杂度为 。
倍增查询第 小代码:
int kth(int k){
int p = 1, ans = 0, sum = 0;
for (;(p << 1) <= n;p <<= 1);
while (p){
if (ans + p <= n && sum + s[ans + p] < k) ans += p, sum += s[ans];
p >>= 1;
}
return ans + 1;
}
树状数组的拓展
区间修改单点查询
树状数组所擅长的是单点修改区间查询,如果我们把操作换成区间修改单点查询,该怎么办呢?
我们可以使用差分来解决这个问题。
的差分数组 满足以下条件:
- 。
- 对于 ,。
我们可以发现,。这样一来,我们想查询 的时候,只需要查询 数组的前缀和即可。
那修改怎么办呢?
我们假设 ,要将 加上 。
我们可以发现,对于差分序列 ,我们只需要使 , 即可。
模板题二代码:
#include <cstdio>
using namespace std;
int n, m;
long long t[500001], ss, l, r;
char c;
long long lowbit(long long x){
return x & -x;
}
long long sum(int i){
long long s = 0;
while (i > 0){
s += t[i];
i -= lowbit(i);
}
return s;
}
void add(int i, long long x){
while (i <= n){
t[i] += x;
i += lowbit(i);
}
}
int main(){
scanf("%d%d", &n, &m);
for (int i = 1;i <= n;i ++) scanf("%lld", &r), add(i, r - l), l = r;
for (int i = 1;i <= m;i ++){
scanf(" %c", &c);
if (c == '1') scanf("%d%d%lld", &l, &r, &ss), add(l, ss), add(r + 1, -ss);
else scanf("%lld", &l), printf("%lld\n", sum(l));
}
}
区间修改区间查询
考虑在区间修改单点查询的基础上继续升级。
如果单点查询 的结果为 ,那查询区间 的结果即为:
上式可以改写为:
我们增加一个树状数组用来维护 ,就可以用两个树状数组来实现区间修改区间查询了!(查询 还是只需查询 即可)
模板题三代码:
#include <algorithm>
#include <cstdio>
#define ll long long
inline int lowbit(int x){return x & -x;}
struct BIT{
int n;
ll s[100001];
void init(int x){
n = x;
for (int i = 1;i <= n;i ++) s[i] = 0;
}
void update(int p, ll x){
while (p <= n) s[p] += x, p += lowbit(p);
}
ll query(int p){
ll sum = 0;
while (p) sum += s[p], p -= lowbit(p);
return sum;
}
}t1, t2;
using namespace std;
int n, m, op, l, r;
ll x;
int main(){
scanf("%d%d", &n, &m);
t1.init(n), t2.init(n);
for (int i = 1;i <= n;i ++){
scanf("%d", &r);
t1.update(i, r - l), t2.update(i, 1ll * (r - l) * i);
l = r;
}
while (m --){
scanf("%d", &op);
if (op == 1){
scanf("%d%d%lld", &l, &r, &x);
t1.update(l, x), t1.update(r + 1, -x);
t2.update(l, 1ll * l * x), t2.update(r + 1, 1ll * (r + 1) * -x);
}
else {
scanf("%d%d", &l, &r);
ll L = 1ll * l * t1.query(l - 1) - t2.query(l - 1);
ll R = 1ll * (r + 1) * t1.query(r) - t2.query(r);
printf("%lld\n", R - L);
}
}
}