树状数组

· · 算法·理论

前置知识

求二进制中 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_xk,再将 x 加上最低位,直到 x>n

x=3 为例,3=(0011)_2(0011)_2\rightarrow(0100)_2\rightarrow(1000)_2,所以将 a_3a_4a_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)