树状数组
树状数组
前言:树状数组可以做的事情基本上线段树都可以做到,只要题目不卡常,用线段树基本都能做。
初步感知
我们先举个例子,如果我们想知道 \(a[1 \dots 7]\) 的和,应该怎么办?
当然可以直接暴力枚举前七个数,相加。
但是如果我们知道 \(A = a_1 + a_2 + a_3 + a_4, B = a_5 + a_6, C = a_7\),那么答案就是 \(A + B + C\)。
所以我们可以得到一个结论:我们总能把 \([1, n]\) 拆成 不多于 \(\boldsymbol{\log n}\) 段区间,使得这 \(\log n\) 段区间的信息是 已知的。
所以,我们只需要合并 \(\log n\) 段区间的信息就可以了。
区间管辖范围
我们可以发现,\(c\) 就是存储某些元素的前缀和的数组,那么,我们应该如何知道 \(c_i\) 管辖的范围呢?
在树状数组中,规定 \(c_i\) 的管辖范围是 \(2 ^ k\),其中 \(k\) 是 \(i\) 的二进制中最低位的 \(1\) 的位数。
我们记 \(lowbit(i)\) 表示 \(i\) 的二进制中最低位的 \(1\) 的位数。
那么,这个 \(2 ^ k\) 应该怎么求呢?
我们定义 \(lowbit(i)\) 表示 \(i\) 的最低位的 \(1\) 以及后面的 \(0\) 所组成的数。
所以,根据我们的位运算知识,可以发现 lowbit(x) = x & -x
。
int lowbit(int x) {
return x & (-x);
}
建树
\(O(n)\) 建树。
方法一:
每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新自己的直接父亲。
void init() {
for (int i = 1; i <= n; i++) {
t[i] += a[i];
int j = i + lowbit(i);
if (j <= n) t[j] += t[i];
}
}
方法二:
由于 \(c_i\) 管辖的是 \([i - lowbit(i) + 1, i]\),所以我们可以维护一个前缀和数组 \(sum\) 来计算 \(c\) 数组。
void init() {
for (int i = 1; i <= n; i++) {
t[i] = sum[i] - sum[i - lowbit(i)];
}
}
区间查询
我们来举个例子,如果我们要求出区间 \([3, 7]\) 的和,应该怎么办呢?
我们可以求出 \([1, 7]\) 的前缀和与 \([1, 2]\) 的前缀和,再相减即可。
所以,有时候,我们可以把区间问题变成前缀问题来解决。
那么,我们应该怎么用树状数组来求出前缀和呢?
由于 \(c_i\) 管辖的区间是 \([i - lowbit(i) + 1, i]\),所以下一个需要合并的区间的右端点是 \(i - lowbit(i)\),那么,我们只要每次都这样暴力的跳,然后边跳边合并 \(c\) 就可以求出前缀和。
时间复杂度为 \(O(\log n)\)。
int getans(int x) {
int ret = 0;
while (x > 0) ret += tr[x], x -= lowbit(x);
return ret;
}
单点修改
那么,如果我们需要修改 \(a_x\) 呢?
修改 \(a_x\) 自然会让那些包含 \(a_x\) 的 \(c_i\) 的答案变化,所以,我们应该如何找到这些 \(i\) 呢?
显然,在树状数组所建成的这棵树上,\(i\) 是 \(x\) 的祖先,所以,我们只需要不停的找父亲,然后改变 \(c_i\) 就可以了。
时间复杂度为 \(O(\log n)\)。
void modify(int x, int k) {
while (x <= n) tr[x] += k, x += lowbit(x);
}
既然我们学会了单点修改,那么我们就可以把建树的过程变成 \(n\) 次区间修改,时间复杂度为 \(O(n \times \log n)\)。
区间修改,单点查询
我们不维护原数组,而是考虑维护差分数组,由于差分数组在处理区间修改上非常方便,只需要在 \(l, r + 1\) 两个点上分别进行修改,并且通过差分数组还原原数组的方式就是前缀和,刚好符合树状数组只能维护前缀和和单点修改的性质,在某些时候可以以优秀的常数和较短的码量代替线段树。
void modify(int x, int k) {
while (x <= n) tr[x] += k, x += lowbit(x);
}
void Modify(int l, int r, int x) {
modify(l, x), modify(r + 1, -x);
}
int getans(int x) {
int ret = 0;
while (x > 0) ret += tr[x], x -= lowbit(x);
return ret;
}
区间修改,区间查询
我们的思路还是将在原序列 \(a\) 上的操作转成在差分数组 \(d\) 上的操作。
先考虑区间查询,拿查询区间和为例,将区间和转换为两个前缀和:\(\sum\limits_{i = l} ^ r a_i = \sum\limits_{i = 1} ^ {r} a_i - \sum\limits_{i = 1} ^ {l - 1} a_i\)。
由于每一个 \(a_i = \sum\limits_{j = 1} ^ i d_j\),所以 \(\sum\limits_{i = 1} ^ r = \sum\limits_{i = 1} ^ r \sum\limits_{j = 1} ^ i d_j = \sum\limits_{i = 1} ^ r (r - i + 1)d_i = (r + 1)\sum\limits_{i = 1} ^ r d_i - \sum\limits_{i = 1} ^ r i \cdot d_i\)
因此,我们只需要分别用两个树状数组维护 \(d_i\) 与 \(i \cdot d_i\) 的信息即可。
我们再考虑区间修改,同样的,由于是维护差分数组,我们只需要对 \(l, r + 1\) 进行修改。
首先,\(d_i\) 的维护和前面是一样的。
而 \(i \cdot d_i\) 的维护也只是在 \(l\) 的位置加上 \(l \cdot x\),并在 \(r + 1\) 的位置减去 \((r + 1) \cdot x\)。
int lowbit(int x) {
return x & (-x);
}
void modify(int x, ll k, int tp) {
while (x <= n) (!tp ? tr[x] : _tr[x]) += k, x += lowbit(x);
}
ll getans(int x, int tp) {
ll ret = 0;
while (x > 0) ret += (!tp ? tr[x] : _tr[x]), x -= lowbit(x);
return ret;
}
void Modify(int x, int y, int k) {
modify(x, k, 0), modify(y + 1, -k, 0);
modify(x, 1ll * x * k, 1);
modify(y + 1, -1ll * (y + 1) * k, 1);
}
ll getsum(int x) {
return (x + 1) * getans(x, 0) - getans(x, 1);
}
ll query(int x, int y) {
return getsum(y) - getsum(x - 1);
}