树状数组
jesse1216
·
·
算法·理论
前置知识
求二进制中 x 的最低位,\text{lowbit}(x)=x\ \&\ (-x)。
算法介绍
单点修改,区间查询:给定一个 n 项的数列 a,你需要实现两种操作:(1) 将 a_x 加上 y;(2) 求出 a_x+a_{x+1}+\dots+a_y 的值。数据范围 1\le n,m\le10^6。
1. 前缀和
令 s_i=a_1+a_2+\dots+a_i,于是 a_x+a_{x+1}+\dots+a_y=s_y-s_{x-1},可以 O(1) 得出。但修改时需要把 s_x,s_{x+1},\dots,s_n 全部加 y,复杂度为 O(n)。
在最差情况下,修改次数很多,这时复杂度为 O(nm),无法通过。
2. 分块(优化的前缀和)
把数列分为 k 段,每一段 \frac{n}{k} 个数,对每一段分别统计前缀和。
在每次查询时,中间的整块(最多有 k 块)可以直接加上,两侧的块也可以前缀和相减得到,所以复杂度为 O(k)。
在每次修改时,只需要把 x 所在的块的前缀和数组修改,复杂度 O(\frac{n}{k})。
所以总复杂度最高为 O(m\cdot\max(k,\frac{n}{k}))。显然 k=\frac{n}{k},即 k=\sqrt{n} 时最小,复杂度 O(m\sqrt{n})。
这个算法可以通过大部分数据,但 n=m=10^6 时,m\sqrt{n}=10^9,无法通过。
3. 树状数组
用二进制优化前缀和:
\begin{cases}
s_1=(a_1)\\
s_2=(a_1+a_2)\\
s_3=(a_1+a_2)+(a_3)\\
s_4=(a_1+a_2+a_3+a_4)\\
s_5=(a_1+a_2+a_3+a_4)+(a_5)\\
s_6=(a_1+a_2+a_3+a_4)+(a_5+a_6)\\
s_7=(a_1+a_2+a_3+a_4)+(a_5+a_6)+(a_7)\\
s_8=(a_1+a_2+a_3+a_4+a_5+a_6+a_7+a_8)\\
\end{cases}
我们把 s_i 按照 i 的二进制分成了若干个部分,例如 5=(101)_2,所以把 s_5 分为了 4 个数加上 1 个数。
构造 t 数列:
\begin{cases}
t_1=a_1\\
t_2=a_1+a_2\\
t_3=a_3\\
t_4=a_1+a_2+a_3+a_4\\
t_5=a_5\\
t_6=a_5+a_6\\
t_7=a_7\\
t_8=a_1+a_2+a_3+a_4+a_5+a_6+a_7+a_8\\
\end{cases}
仔细观察上面的图,可以发现如下规律:
从 x 开始,每次加上 t_x,再将 x 减去最低位,直到 x=0。
以 x=7 为例,7=(111)_2,(111)_2\rightarrow(110)_2\rightarrow(100)_2\rightarrow(000)_2,所以 s_7=t_7+t_6+t_4。
从 x 开始,每一次将 t_x 加 k,再将 x 加上最低位,直到 x>n。
以 x=3 为例,3=(0011)_2,(0011)_2\rightarrow(0100)_2\rightarrow(1000)_2,所以将 a_3、a_4、a_8 加上 k 即可。
这两个操作的复杂度均为 O(\log n),因为每操作一次都能“搞定” x 的一位。
代码
```cpp
#include <iostream>
#define int long long
using namespace std;
const int N = 5e5 + 5;
int n, m, op, x, y, a[N];
inline int lowbit(int x) {
return x & (-x);
}
inline void Change(int x, int k) {
while (x <= n) {
a[x] += k;
x += lowbit(x);
}
}
inline int Query(int x) {
int res = 0;
while (x != 0) {
res += a[x];
x -= lowbit(x);
}
return res;
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> x;
Change(i, x);
}
for (int i = 1; i <= m; i++) {
cin >> op >> x >> y;
if (op == 1) Change(x, y);
else cout << Query(y) - Query(x - 1) << endl;
}
return 0;
}
```
## 应用
### 区间修改,单点查询
> 给定一个 $n$ 项的数列 $a$,你需要实现两种操作:$(1)$ 将 $a_x\sim a_y$ 加上 $k$;$(2)$ 求出 $a_x$ 的值。数据范围 $1\le n,m\le10^6$。
设 $b$ 为 $a$ 的差分数组。修改操作转换为 $b_x$ 加 $k$、$b_{y+1}$ 减 $k$,而查询操作即 $b_1+b_2+\dots b_n$。这就可以用**单点修改,区间查询**的方法解决了。
### 区间修改,区间查询
> 给定一个 $n$ 项的数列 $a$,你需要实现两种操作:$(1)$ 将 $a_x\sim a_y$ 加上 $k$;$(2)$ 求出 $a_x+a_{x+1}+\dots+a_y$ 的值。$n,m\le10^6$。
设 $b$ 为 $a$ 的差分数组,则:
$$
\begin{aligned}
\sum_{i=1}^ma_i
&= \sum_{i=1}^m\sum_{j=1}^ib_j \\
&= \sum_{j=1}^mb_j\times(m-j+1) \\
&= \sum_{j=1}^mb_j\times(m+1)-\sum_{j=1}^mb_j\times j \\
&= (m+1)\sum_{j=1}^mb_j-\sum_{j=1}^mb_j\times j
\end{aligned}
$$
开两个树状数组分别维护 $b_i$ 和 $i\times b_i$ 即可。
对区间 $[x,y]$ 加 $k$ 时:
- 对于 $b_i$ 的树状数组,对 $x$ 加 $k$,$y+1$ 减 $k$。
- 对于 $i\times b_i$ 的树状数组,对 $x$ 加 $kx$,$y+1$ 减 $k(y+1)$。
#### 代码
```cpp
#include <iostream>
#define int long long
using namespace std;
const int N = 2e5 + 5;
int n, m, op, x, y, k, a[N], t1[N], t2[N];
inline int lowbit(int x) {
return x & (-x);
}
inline void Change(int x, int k) {
int tmp = x - 1;
while (x <= n) {
t1[x] += k;
t2[x] += tmp * k;
x += lowbit(x);
}
}
inline int Query(int x) {
int res = 0, tmp = x;
while (x != 0) {
res += (tmp * t1[x] - t2[x]);
x -= lowbit(x);
}
return res;
}
signed main(void) {
ios::sync_with_stdio(false); cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
Change(i, a[i] - a[i - 1]);
}
for (int i = 1; i <= m; i++) {
cin >> op;
if (op == 1) {
cin >> x >> y >> k;
Change(x, k), Change(y + 1, -k);
}
else {
cin >> x >> y;
cout << Query(y) - Query(x - 1) << endl;
}
}
return 0;
}
```
## 题目练习
### 模板和基础应用
- [P3374 【模板】树状数组 1](https://www.luogu.com.cn/problem/P3374)
- [P3368 【模板】树状数组 2](https://www.luogu.com.cn/problem/P3368)
- [P2068 统计和](https://www.luogu.com.cn/problem/P2068)
- [P2184 贪婪大陆](https://www.luogu.com.cn/problem/P2184)
- [P2357 守墓人](https://www.luogu.com.cn/problem/P2357)
- [P4939 Agent2](https://www.luogu.com.cn/problem/P4939)
### 进阶应用
- [P1908 逆序对](https://www.luogu.com.cn/problem/P1908)
- [P1966 火柴排队](https://www.luogu.com.cn/problem/P1966)
- [P1972 HH的项链](https://www.luogu.com.cn/problem/P1972)
- [P5057 简单题](https://www.luogu.com.cn/problem/P5057)
- [P5459 回转寿司](https://www.luogu.com.cn/problem/P5459)
- [P6225 异或橙子](https://www.luogu.com.cn/problem/P6225)