数据结构------树状数组和在线、离线操作
- [数据结构 树状数组](#数据结构 树状数组)
-
- 树状数组和线段树的对比
- 树状数组维护区间的原理
- 一维树状数组
-
- [单点修改 + 区间查询](#单点修改 + 区间查询)
-
- [#130. 树状数组 1 - LibreOJ](#130. 树状数组 1 - LibreOJ)
- [区间修改 + 单点查询](#区间修改 + 单点查询)
-
- [131. 树状数组 2 - LibreOJ](#131. 树状数组 2 - LibreOJ)
- [区间修改 + 区间查询](#区间修改 + 区间查询)
-
- [#132. 树状数组 3 - LibreOJ](#132. 树状数组 3 - LibreOJ)
- 二维树状数组
-
- [单点修改 + 区间查询](#单点修改 + 区间查询)
-
- [#133. 二维树状数组 1 - LibreOJ](#133. 二维树状数组 1 - LibreOJ)
- [区间修改 + 单点查询](#区间修改 + 单点查询)
-
- [#134. 二维树状数组 2 - LibreOJ](#134. 二维树状数组 2 - LibreOJ)
- [区间修改 + 区间查询](#区间修改 + 区间查询)
-
- [#135. 二维树状数组 3 - LibreOJ](#135. 二维树状数组 3 - LibreOJ)
- 树状数组的应用列举
-
- [P1908 逆序对 - 洛谷](#P1908 逆序对 - 洛谷)
- [P1966 火柴排队 - 洛谷](#P1966 火柴排队 - 洛谷)
- [P10589 楼兰图腾 - 洛谷](#P10589 楼兰图腾 - 洛谷)
- [P3605 Promotion Counting P - 洛谷](#P3605 Promotion Counting P - 洛谷)
- [P4054 计数问题 - 洛谷](#P4054 计数问题 - 洛谷)
- [P3586 物流 Logistics - 洛谷 前缀和](#P3586 物流 Logistics - 洛谷 前缀和)
- 在线操作与离线操作
-
- 时间轴和动、静态问题
- 在线、离线操作
- [P1972 HH 的项链 - 洛谷](#P1972 HH 的项链 - 洛谷)
- [P4113 采花 - 洛谷](#P4113 采花 - 洛谷)
- OJ参考
数据结构 树状数组
树状数组(Binary Indexed Tree 简称:BIT)是一种只支持"单点修改"和"区间查询"的数据结构,修改和查询的时间复杂度为 log n \log n logn 级别。
但有的问题能通过转化转换成单点修改和区间查询时也能用树状数组解决。
树状数组尽管在使用上和线段树没有联系,但理解上需要有线段树的基础,且要求读者会使用差分和前缀和解决问题。
树状数组能解决的问题是线段树能解决的问题的子集:
- 树状数组能解决的,线段树一定能解决;
- 线段树能解决的,树状数组不一定可以。
树状数组的代码要远比线段树短,虽然时间复杂度都是 O ( log n ) \text{O}(\log n) O(logn) ,但树状数组的时间效率常数也更小,因为线段树在最差情况下需要遍历左、右子树加右子树的根结点,实际枚举次数是 2 log n + m 2\log n+m 2logn+m ,而树状数组则是 ≤ log n \le \log n ≤logn 。此外有的问题会将数据量控制在使用线段树会超时,但使用树状数组刚好能过的范围。
树状数组一般维护的信息需要满足结合律 以及可差分(或者说是可减性),比如区间,大区间的和减去小区间的和可得到另一些小区间的和,以及区间乘积。如果不满足交换律和可差分,树状数组就不能维护,比如区间最值以及区间 gcd 等。
树状数组和线段树的对比
对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:
- 单点修改:修改某一个位置 i i i 的值;
- 区间查询:查询区间 [ l , r ] [l, r] [l,r] 的和。
若 n = 8 n = 8 n=8,用线段树可以这样维护:

其中,每一个结点存储的就是区间和信息。但若维护的是区间和 ,很多区间其实没有存储数据的必要 。比如 [ 3 , 4 ] [3, 4] [3,4] 的区间和,可以通过 [ 1 , 4 ] [1, 4] [1,4] 的区间和减去 [ 1 , 2 ] [1, 2] [1,2] 的区间和得出。此时删掉不必要的信息之后,可以得到如下结构,仅需用数组就可以存储:

通过上述过程可知,树状数组可以看作是"简化版的线段树"。在维护信息时,发现某些信息不需要去维护,进而将线段树中某些结点删掉,仅用一个数组就可以维护出所有想要的区间信息。
此时也可以发现,若查询的是区间最大值最小值,树状数组就不可行了。原因就是不能通过区间相减得出被删除区间的最小值。
树状数组维护区间的原理
树状数组可以通过用二进制将一段区间和拆分成若干没有交集的区间,进而得到树状数组。但通过线段树删减结点的方式更容易理解树状数组是如何维护信息的。
树状数组的下标需要从 1 开始计数。此时才能有如下性质:
-
向上爬公式(找父亲):结点编号为 x x x 时, x + lowbit ( x ) x + \text{lowbit}(x) x+lowbit(x) 等于父结点的编号。 lowbit ( x ) \text{lowbit}(x) lowbit(x) 为获取 x x x 的二进制 01 序列里最右边的 1 。
例如 3 的二进制是 011 ,它最右边的 1 是 001 ,则它的父结点就是 3 + lowbit ( 3 ) = 4 3+\text{lowbit}(3)=4 3+lowbit(3)=4 , 父结点的父结点就是 4 + lowbit ( 4 ) = 8 4+\text{lowbit}(4)=8 4+lowbit(4)=8。
-
向前跳公式(找前一个相邻区间):结点编号为 x x x 时, x − lowbit ( x ) x - \text{lowbit}(x) x−lowbit(x) 等于前一个相邻区间所在的编号。
例如 7 的二进制是 111 , 7 − lowbit ( 7 ) 7-\text{lowbit}(7) 7−lowbit(7) 后是 6,正好是区间 [ 5 , 6 ] [5,6] [5,6] 。同理 7 − lowbit ( 7 ) − lowbit ( lowbit(7) ) = 4 7-\text{lowbit}(7)-\text{lowbit}(\text{lowbit(7)})=4 7−lowbit(7)−lowbit(lowbit(7))=4 ,正好是 [ 1 , 4 ] [1,4] [1,4] 区间。
-
维护的区间:结点编号为 x x x 时,维护的区间信息为 [ x − lowbit ( x ) + 1 , x ] [x - \text{lowbit}(x) + 1, x] [x−lowbit(x)+1,x]。
例如 6 ,它维护的区间是 [ 6 − lowbit ( 6 ) + 1 , 6 ] = [ 5 , 6 ] [6-\text{lowbit}(6)+1,6]=[5,6] [6−lowbit(6)+1,6]=[5,6] 。

所以任意正整数都有关于 2 的不重复次幂的唯一分解序列,一个正整数最多可分成 O ( log x ) \text{O}(\log x) O(logx) 个小区间。
即 x = 2 i 1 + 2 i 2 + ⋯ + 2 i m x=2^{i_1}+2^{i_2}+\dots+2^{i_m} x=2i1+2i2+⋯+2im ,则 [ 1 , x ] [1,x] [1,x] 可分成的小区间个数为 x x x 的二进制序列中 1 的个数。例如不同长度的区间:
长度为 2 i 1 2^{i_1} 2i1 的小 区间 [ 1 , 2 i 1 ] [1,2^{i_1}] [1,2i1] 。
长度为 2 i 2 2^{i_2} 2i2 的小 区间 [ 2 i 1 + 1 , 2 i 1 + 2 i 2 ] [2^{i_1}+1,2^{i_1}+2^{i_2}] [2i1+1,2i1+2i2] 。
长度为 2 i 3 2^{i_3} 2i3 的小 区间 [ 2 i 1 + 2 i 2 + 1 , 2 i 1 + 2 i 2 + 2 i 3 ] [2^{i_1}+2^{i_2}+1,2^{i_1}+2^{i_2}+2^{i_3}] [2i1+2i2+1,2i1+2i2+2i3] 。
... \dots ...
长度为 2 i m 2^{i_m} 2im 的小 区间 [ 2 i 1 + 2 i 2 + ⋯ + 2 i m − 1 + 1 , 2 i 1 + 2 i 2 + ⋯ + 2 i m ] [2^{i_1}+2^{i_2}+\dots+2^{i_{m-1}}+1,2^{i_1}+2^{i_2}+\dots+2^{i_m}] [2i1+2i2+⋯+2im−1+1,2i1+2i2+⋯+2im] 。
这里 [ 1 , 8 ] [1,8] [1,8] 就被划分成 [ 1 , 2 1 ] [1,2^1] [1,21] 、 [ 2 1 + 1 , 2 2 ] [2^1+1,2^2] [21+1,22] 和 [ 2 2 + 1 , 2 3 ] [2^2+1,2^3] [22+1,23] 3 个区间。
其中 lowbit(x)=x&-x 。例如 3 在计算机中的存储是 0 ... 011 0\dots011 0...011 ,而 − 3 -3 −3 在计算机中存储的是补码,即 − 3 -3 −3 的原码是 10 ⋯ 011 10\cdots011 10⋯011 ,而它的补码是二进制原码先取反变成 1 ... 100 1\dots100 1...100 ,然后再加 1 1 1 变成 1 ... 101 1\dots101 1...101 。
这里通过一个代码直观了解这个操作的原理。
cpp
#include <bits/stdc++.h>
using namespace std;
template <typename T>
void getbit(T x) { // 函数模板,可接收任意整型
for (int i = sizeof(T) * 8 - 1; i >= 0; i--) {
if ((x >> i) & 1)
cout << 1;
else
cout << 0;
}
cout << endl;
}
int main() {
int x = 3, y = -3;
getbit(x);
getbit(y);
return 0;
}
输出:
00000000000000000000000000000011
11111111111111111111111111111101
这里可以看到, 3 在计算机中的存储方式就是 0 ... 011 0\dots011 0...011 , − 3 -3 −3 则是 1 ... 101 1\dots 101 1...101 ,二者相与就是 3 最右边的 1 。
一维树状数组
单点修改 + 区间查询
对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:
- 修改某一个位置 i i i 的值;
- 查询区间 [ l , r ] [l, r] [l,r] 的和。
这个操作是树状数组最核心的操作,几乎所有用树状数组解决的问题都围绕单点修改 + + + 区间查询展开。哪怕是区间修改也要转换成 2 个单点修改。
单点修改 :以 x x x 位置的数据增加一个值为例,当某一个位置 x x x 的值发生改变之后,沿着这个点一路向上的点都会发生改变。因此,从 x x x 开始,不断向上爬,一边爬一遍修改对应结点的值即可。
cpp
inline int Bitree::lowbit(int x) {
return x & -x;
}
void Bitree::modify(int x, int k) {
for (int i = x; i < bt.size(); i += lowbit(i))
bt[i] += k;
}
创建 :默认整个数组全部为 0,每读入一个数 a [ i ] a[i] a[i],就相当于 i i i 位置的数增加了 a [ i ] a[i] a[i]。因此,调用 modify 函数就可以完成树状数组的创建。这里依旧延续线段树的进行封装的习惯。
cpp
#include <bits/stdc++.h>
using namespace std;
struct Bitree {
vector<int> bt; // 树状数组本体
Bitree(int sz) {
bt.resize(sz + 1, 0); // 下标从1开始
}
inline int lowbit(int x) {
return x & -x;
}
void modify(int x, int k) {
for (int i = x; i < bt.size(); i += lowbit(i))
bt[i] += k;
}
};
int main() {
int n;
cin >> n;
Bitree bt(n);
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
bt.modify(i, x);
}
return 0;
}
区间查询 :通过向前跳性质可以得出,树状数组可以快速查询 [ 1 , x ] [1, x] [1,x] 区间的和。例如想查询 [ x , y ] [x, y] [x,y] 的区间和,可以用前缀和的思想,先求出 [ 1 , x − 1 ] [1, x - 1] [1,x−1] 的区间和,再求出 [ 1 , y ] [1, y] [1,y] 的区间和,后者减去前者即可,即 s u m ( x , y ) = q u e r y ( y ) − q u e r y ( x − 1 ) sum(x,y)=query(y)-query(x-1) sum(x,y)=query(y)−query(x−1) 。
cpp
inline int Bitree::lowbit(int x){
return x&-x;
}
int Bitree::query(int x) {
int sum = 0;
for (int i = x; i; i -= lowbit(i))
sum += bt[i];
return sum;
}
#130. 树状数组 1 - LibreOJ
#130. 树状数组 1 :单点修改,区间查询 - 题目 - LibreOJ
模板题。
cpp
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;
struct Bitree {
vl bt;// 树状数组本体
Bitree(LL sz) {
bt.resize(sz + 1, 0);
}
inline LL lowbit(LL x) {
return x & -x;
}
void modify(LL x, LL k) {
for (LL i = x; i < bt.size(); i += lowbit(i))
bt[i] += k;
}
LL query(LL x) {
LL sum = 0;
for (LL i = x; i; i -= lowbit(i))
sum += bt[i];
return sum;
}
};
int main() {
// freopen("in.in", "r", stdin);
int n, q;
cin >> n >> q;
Bitree bt(n);
for (int i = 1; i <= n; i++) {
LL x;
cin >> x;
bt.modify(i, x);
}
for (int T = q; T--;) {
int op, x, y;
cin >> op >> x >> y;
if (op == 1)
bt.modify(x, y);
else
cout << bt.query(y) - bt.query(x - 1) << '\n';
}
return 0;
}
区间修改 + 单点查询
对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:
- 将区间 [ x , y ] [x, y] [x,y] 上所有数都增加 d d d;
- 查询 a i a_i ai 的值。
树状数组和线段树不同,因为很多结点被砍掉,所以无法使用延迟标记处理区间修改,若强行使用遍历区间的方式修改,则时间复杂度会退化到 O ( n log n ) \text{O}(n\log n) O(nlogn) 。
此时利用差分,创建出原始数组的差分数组,使用树状数组维护差分数组,那么上述两个操作:
- 原数组的区间修改变成:差分数组 x x x 位置增加 d d d, y + 1 y+1 y+1 位置增加 − d -d −d;
- 原数组的单点查询变成:差分数组求 [ 1 , i ] [1, i] [1,i] 的区间和。
此时就可以用树状数组来解决。
131. 树状数组 2 - LibreOJ
#131. 树状数组 2 :区间修改,单点查询 - 题目 - LibreOJ
模板题。
cpp
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;
struct Bitree {
vl bt;
Bitree(LL sz) {
bt.resize(sz + 1, 0);
}
size_t size() {
return bt.size();
}
inline LL lowbit(LL x) {
return x & -x;
}
void modify(LL x, LL k) {
for (LL i = x; i < bt.size(); i += lowbit(i))
bt[i] += k;
}
LL query(LL x) {
LL sum = 0;
for (LL i = x; i; i -= lowbit(i))
sum += bt[i];
return sum;
}
};
int main() {
// freopen("in.in", "r", stdin);
int n, q;
cin >> n >> q;
Bitree bt(n + 1); // 差分数组需要多开一个单位
for (int i = 1; i <= n; i++) {
LL x;
cin >> x;
bt.modify(i, x); // 树状数组维护差分数组
bt.modify(i + 1, -x);
}
for (int T = q; T--;) {
int op, x, y, z;
cin >> op >> x;
if (op == 1) {
cin >> y >> z;
bt.modify(x, z);
if (y + 1 < bt.size())
bt.modify(y + 1, -z);
} else {
cout << bt.query(x) << '\n';
}
}
return 0;
}
区间修改 + 区间查询
推荐这类问题优先使用线段树,除非超时。
对于一个长度为 n n n 的序列,执行 m m m 次操作,每次操作为:
- 把区间 [ x , y ] [x, y] [x,y] 所有的数统一加 k k k;
- 查询区间 [ x , y ] [x, y] [x,y] 所有元素的和。
可以利用差分,将区间修改改成两个单点修改。那么区间查询操作也需要在差分数组中进行计算。
设原始数组为 a a a,差分数组为 d d d,那么:
1 , i \] \[1, i\] \[1,i\] 的区间和:
s u m \[ 1 , i \] = a \[ 1 \] + a \[ 2 \] + a \[ 3 \] + ⋯ + a \[ i \] = d \[ 1 \] + ( d \[ 1 \] + d \[ 2 \] ) + ⋯ + ( d \[ 1 \] + d \[ 2 \] + ⋯ + d \[ i \] ) = d \[ 1 \] × ( i − 0 ) + d \[ 2 \] × ( i − 1 ) + ⋯ + d \[ i \] × ( i − ( i − 1 ) ) = ( d \[ 1 \] + d \[ 2 \] + ⋯ d \[ i \] ) × i − ( d \[ 1 \] × 0 + d \[ 2 \] × 1 + d \[ 3 \] × 2 + ⋯ + d \[ i \] × ( i − 1 ) ) \\begin{aligned}sum\[1, i\] \&= a\[1\] + a\[2\] + a\[3\] + \\cdots + a\[i\]\\\\\&=d\[1\] + (d\[1\] + d\[2\]) + \\cdots + (d\[1\] + d\[2\] + \\cdots + d\[i\])\\\\\&=d\[1\] \\times (i-0) + d\[2\] \\times (i - 1) + \\cdots + d\[i\] \\times (i-(i-1))\\\\\&=(d\[1\] + d\[2\] + \\cdots d\[i\]) \\times i - \\\\\&(d\[1\] \\times 0 + d\[2\] \\times 1 + d\[3\] \\times 2 + \\cdots + d\[i\] \\times (i - 1))\\end{aligned} sum\[1,i\]=a\[1\]+a\[2\]+a\[3\]+⋯+a\[i\]=d\[1\]+(d\[1\]+d\[2\])+⋯+(d\[1\]+d\[2\]+⋯+d\[i\])=d\[1\]×(i−0)+d\[2\]×(i−1)+⋯+d\[i\]×(i−(i−1))=(d\[1\]+d\[2\]+⋯d\[i\])×i−(d\[1\]×0+d\[2\]×1+d\[3\]×2+⋯+d\[i\]×(i−1))
所以对区间修改 + + + 区间查询的问题,可以创建 2 个树状数组,一个维护 { d \[ i \] } \\{d\[i\]\\} {d\[i\]} 这个序列的区间和,另一个维护 { d \[ i \] × ( i − 1 ) } \\{d\[i\] \\times (i - 1)\\} {d\[i\]×(i−1)} 这个序列的区间和。
当把区间 \[ x , y \] \[x, y\] \[x,y\] 所有的数统一加 k k k 时,区间和变成
s u m \[ 1 , i \] = d \[ 1 \] + ( d \[ 1 \] + d \[ 2 \] ) + ⋯ + ( d \[ 1 \] + d \[ 2 \] + ⋯ + ( d \[ x \] + k ) + ... ( d \[ y + 1 \] − k ) + ... d \[ i \] ) = d \[ 1 \] × ( i − 0 ) + d \[ 2 \] × ( i − 1 ) + ⋯ + ( d \[ x \] + k ) × ( i − x + 1 ) + ⋯ + ( d \[ y + 1 \] − k ) × ( i − y ) + d \[ i \] × ( i − ( i − 1 ) ) = ( d \[ 1 \] + d \[ 2 \] + ⋯ + ( d \[ x \] + k ) + ⋯ + ( d \[ y + 1 \] − k ) + ... + d \[ i \] ) × i − ( d \[ 1 \] × 0 + d \[ 2 \] × 1 + ⋯ + ( d \[ x \] + k ) × ( x − 1 ) + ⋯ + ( d \[ y + 1 \] − k ) × y + ⋯ + d \[ i \] × ( i − 1 ) ) \\begin{aligned}sum\[1, i\]\&=d\[1\] + (d\[1\] + d\[2\]) + \\cdots + (d\[1\] + d\[2\] + \\cdots\\\\\&+ (d\[x\]+k)+\\dots (d\[y+1\]-k) + \\dots d\[i\])\\\\\\\\\&=d\[1\] \\times (i-0) + d\[2\] \\times (i - 1) + \\cdots\\\\\&+(d\[x\]+k)\\times(i-x+1)+\\dots+(d\[y+1\]-k)\\times (i-y)\\\\\&+ d\[i\] \\times (i-(i-1))\\\\\\\\\&=(d\[1\] + d\[2\] + \\cdots + \\\\\&(d\[x\]+k)+\\dots+(d\[y+1\]-k)+\\dots\\\\\&+ d\[i\])\\times i - \\\\\\\\\&(d\[1\] \\times 0 + d\[2\] \\times 1 + \\cdots +\\\\\&(d\[x\]+k)\\times (x-1) + \\dots +(d\[y+1\]-k)\\times y+\\dots+\\\\\&d\[i\] \\times (i - 1))\\end{aligned} sum\[1,i\]=d\[1\]+(d\[1\]+d\[2\])+⋯+(d\[1\]+d\[2\]+⋯+(d\[x\]+k)+...(d\[y+1\]−k)+...d\[i\])=d\[1\]×(i−0)+d\[2\]×(i−1)+⋯+(d\[x\]+k)×(i−x+1)+⋯+(d\[y+1\]−k)×(i−y)+d\[i\]×(i−(i−1))=(d\[1\]+d\[2\]+⋯+(d\[x\]+k)+⋯+(d\[y+1\]−k)+...+d\[i\])×i−(d\[1\]×0+d\[2\]×1+⋯+(d\[x\]+k)×(x−1)+⋯+(d\[y+1\]−k)×y+⋯+d\[i\]×(i−1))
所以第 1 个树状数组在使区间 \[ x , y \] \[x,y\] \[x,y\] 的所有的数统一加 k k k 时,第 2 个树状数组需要做出对应的操作:在 d \[ x \] × ( x − 1 ) d\[x\]\\times (x-1) d\[x\]×(x−1) 的位置增加一个 k × ( x − 1 ) k\\times (x-1) k×(x−1) ,在 d \[ y + 1 \] × y d\[y+1\]\\times y d\[y+1\]×y 减去一个 k × ( y + 1 − 1 ) k\\times (y+1-1) k×(y+1−1) 。
查询区间 \[ x , y \] \[x,y\] \[x,y\] 时,使用这里推导的公式:
s u m ( 1 , y ) − s u m ( 1 , x − 1 ) = b t 1 ( y ) × y − b t 2 ( y ) − b t 1 ( x − 1 ) × ( x − 1 ) + b t 2 ( x − 1 ) \\begin{aligned}sum(1,y)-sum(1,x-1)\&=bt_1(y)\\times y-bt_2(y)-\\\\\&bt_1(x-1)\\times (x-1)+bt_2(x-1)\\end{aligned} sum(1,y)−sum(1,x−1)=bt1(y)×y−bt2(y)−bt1(x−1)×(x−1)+bt2(x−1)
##### #132. 树状数组 3 - LibreOJ
[#132. 树状数组 3 :区间修改,区间查询 - 题目 - LibreOJ](https://loj.ac/p/132)
还是模板题。
```cpp
#include
查询单个矩阵和的方法:
cpp
int Bitree::query(vvl &bt, int x, int y) {
int sum = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
sum += bt[i][j];
return sum;
}
同样的矩阵和要查询 4 个,然后用二维前缀和的方式进行计算。
#133. 二维树状数组 1 - LibreOJ
#133. 二维树状数组 1:单点修改,区间查询 - 题目 - LibreOJ
模板题。
cpp
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;
using vvl = vector<vl>;
struct Bitree {
vvl bt; // 树状数组本体
Bitree(int n, int m) {
bt.resize(n + 1, vl(m + 1, 0)); // 下标从1开始
}
inline int lowbit(int x) {
return x & -x;
}
void modify(int x, int y, LL k) { // 二维比一维多一层循环
for (int i = x; i < bt.size(); i += lowbit(i))
for (int j = y; j < bt[i].size(); j += lowbit(j))
bt[i][j] += k;
}
LL query(int x, int y) {
LL sum = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
sum += bt[i][j];
return sum;
}
};
int main() {
// freopen("in.in", "r", stdin);
int n, m, op;
int x1, y1, x2, y2;
LL k;
cin >> n >> m;
Bitree bt(n, m);
while (cin >> op) {
cin >> x1 >> y1;
if (op == 1) {
cin >> k;
bt.modify(x1, y1, k);
} else {
cin >> x2 >> y2;
LL p1 = bt.query(x2, y2);
LL p2 = bt.query(x1 - 1, y2);
LL p3 = bt.query(x2, y1 - 1);
LL p4 = bt.query(x1 - 1, y1 - 1);
cout << p1 - p2 - p3 + p4 << '\n';
}
}
return 0;
}
区间修改 + 单点查询
已知一个二维数组,有两种操作:
- 将左上角为 [ x 1 , y 1 ] [x_1, y_1] [x1,y1],右下角为 [ x 2 , y 2 ] [x_2, y_2] [x2,y2] 的子矩阵内所有的数都加上 d d d;
- 查询 [ x , y ] [x, y] [x,y] 格子的值。
区间修改可利用差分算法,创建出原始矩阵的差分矩阵,之后上述两个操作:
- 原矩阵的区间修改变成:差分矩阵中四个位置的修改;
- 原矩阵的单点查询变成:差分矩阵中求以 [ x , y ] [x, y] [x,y] 为右下角的子矩阵的和。
此时就可以用二维树状数组来解决。
差分矩阵的修改:假设原始矩阵 a a a 中,以 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 为左上角, ( x 2 , y 2 ) (x_2, y_2) (x2,y2) 为右下角的子矩阵的每个元素都加上 k k k:

除了紫色部分,其余都是消除修改操作带来的影响。
cpp
using vvl = vector<vector<long long>>;
void dif(vvl &d, int x1, int y1, int x2, int y2, long long k) {
d[x1][y1] += k;
d[x2 + 1][y1] -= k;
d[x1][y2 + 1] -= k;
d[x2 + 1][y2 + 1] += k;
}
#134. 二维树状数组 2 - LibreOJ
#134. 二维树状数组 2:区间修改,单点查询 - 题目 - LibreOJ
模板题。
cpp
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;
using vvl = vector<vl>;
struct Bitree {
vvl bt; // 树状数组本体
Bitree(int n, int m) {
bt.resize(n + 1, vl(m + 1, 0)); // 下标从1开始
}
inline int lowbit(int x) {
return x & -x;
}
void modify(int x, int y, LL k) { // 二维比一维多一层循环
for (int i = x; i < bt.size(); i += lowbit(i))
for (int j = y; j < bt[i].size(); j += lowbit(j))
bt[i][j] += k;
}
LL query(int x, int y) {
LL sum = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
sum += bt[i][j];
return sum;
}
};
int main() {
// freopen("in.in", "r", stdin);
int n, m, op;
int a, b, c, d, x, y;
LL k;
cin >> n >> m;
Bitree bt(n, m);
while (cin >> op) {
if (op == 1) {
cin >> a >> b >> c >> d >> k;
bt.modify(a, b, k);
bt.modify(a, d + 1, -k);
bt.modify(c + 1, b, -k);
bt.modify(c + 1, d + 1, k);
} else {
cin >> x >> y;
cout << bt.query(x, y) << '\n';
}
}
return 0;
}
区间修改 + 区间查询
已知一个二维数组,有两种操作:
- 将左上角为 [ x 1 , y 1 ] [x_1, y_1] [x1,y1],右下角为 [ x 2 , y 2 ] [x_2, y_2] [x2,y2] 的子矩阵内所有的数都加上 d d d;
- 查询左上角为 [ x 1 , y 1 ] [x_1, y_1] [x1,y1],右下角为 [ x 2 , y 2 ] [x_2, y_2] [x2,y2] 的子矩阵内所有的元素的和。
和之前一样,区间修改可以利用差分将区间修改操作改成四个单点修改。
区间查询操作也需要在差分矩阵中获得,方式是先求和得到原数组,原数组再通过前缀和的方式得到,但若直接从差分数组得到,式子太过庞大,需要寻找规律进行简化。
设原数矩阵为 a a a ,它的差分矩阵为 d d d 。原矩阵求 [ x , y ] [x, y] [x,y] 为右下角的区间和时:
- 需要在差分矩阵中的每一个位置都求一次二维前缀和。
- 求原数组的前缀和的过程中,差分矩阵的某项 d [ i ] [ j ] d[i][j] d[i][j] 会在区间 [ i , j ] ∼ [ x , y ] [i, j] \sim [x, y] [i,j]∼[x,y] 的每一个位置求前缀和时都加一次,因此总共加进去的次数为 ( x − i + 1 ) × ( y − j + 1 ) (x - i + 1) \times (y - j + 1) (x−i+1)×(y−j+1) 次,即 ( x − i + 1 ) × ( y − j + 1 ) (x - i + 1) \times (y - j + 1) (x−i+1)×(y−j+1) 个 d [ i ] [ j ] d[i][j] d[i][j] 连续相加作为前缀和计算的一部分。

所以原始矩阵中, ( x , y ) (x,y) (x,y) 为右下角的区间和为
s u m [ x ] [ y ] = ∑ i = 1 x ∑ j = 1 y a [ i ] [ j ] = ∑ i = 1 x ∑ j = 1 y d [ i ] [ j ] × ( x − i + 1 ) × ( y − j + 1 ) = ∑ i = 1 x ∑ j = 1 y ( d [ i ] [ j ] × ( x y + x + y + 1 ) − d [ i ] [ j ] × i × ( y + 1 ) − d [ i ] [ j ] × j × ( x + 1 ) + d [ i ] [ j ] × i × j ) \begin{aligned}sum[x][y]=&\sum\limits_{i=1}^x \sum\limits_{j=1}^y a[i][j]\\=&\sum\limits_{i=1}^x \sum\limits_{j=1}^y d[i][j] \times (x - i + 1) \times (y - j + 1)\\=&\sum\limits_{i=1}^x \sum\limits_{j=1}^y (d[i][j] \times (xy + x + y + 1) \\&- d[i][j] \times i \times (y + 1)\\&- d[i][j] \times j \times (x + 1)\\&+ d[i][j] \times i \times j)\end{aligned} sum[x][y]===i=1∑xj=1∑ya[i][j]i=1∑xj=1∑yd[i][j]×(x−i+1)×(y−j+1)i=1∑xj=1∑y(d[i][j]×(xy+x+y+1)−d[i][j]×i×(y+1)−d[i][j]×j×(x+1)+d[i][j]×i×j)
x x x 和 y y y 都是已知常数,所以求目标矩阵需要使用 4 个树状数组,分别维护 d [ i ] [ j ] d[i][j] d[i][j]、 d [ i ] [ j ] × i d[i][j] \times i d[i][j]×i、 d [ i ] [ j ] × j d[i][j] \times j d[i][j]×j、 d [ i ] [ j ] × i × j d[i][j] \times i \times j d[i][j]×i×j 。
当然,这只是利用差分矩阵求左上角为 ( 1 , 1 ) (1,1) (1,1) ,右下角为 ( x , y ) (x,y) (x,y) 的原矩阵的区间和的求法,若要求左上角不为 ( 1 , 1 ) (1,1) (1,1) 的原矩阵的区间和,还得使用二维前缀和的方式求解。
#135. 二维树状数组 3 - LibreOJ
#135. 二维树状数组 3:区间修改,区间查询 - 题目 - LibreOJ
模板题,但思维量和整体难度放在洛谷的体系也是不低于蓝题,甚至紫题。
cpp
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;
using vvl = vector<vl>;
struct Bitree {
vvl bt;
inline int lowbit(int x) {
return x & -x;
}
void modify(int x, int y, LL k) {
for (int i = x; i < bt.size(); i += lowbit(i))
for (int j = y; j < bt[i].size(); j += lowbit(j))
bt[i][j] += k;
}
LL query(int x, int y) {
LL sum = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
sum += bt[i][j];
return sum;
}
};
Bitree bt, bi, bj, bij;
// 对应4种形式的差分数组
void init(int n, int m) {
bt.bt.resize(n + 2, vl(m + 2, 0));
bi.bt = bj.bt = bij.bt = bt.bt;
}
void add(int x, int y, LL k) {
bt.modify(x, y, k);
bi.modify(x, y, x * k);
bj.modify(x, y, y * k);
bij.modify(x, y, x * y * k);
}
LL sum(int x, int y) {
LL p1 = bt.query(x, y) * (x * y + x + y + 1);
LL p2 = bi.query(x, y) * (y + 1);
LL p3 = bj.query(x, y) * (x + 1);
LL p4 = bij.query(x, y);
return p1 - p2 - p3 + p4;
}
int main() {
// freopen("in.in", "r", stdin);
int n, m;
int op, a, b, c, d;
LL k;
cin >> n >> m;
init(n, m);
while (cin >> op) {
cin >> a >> b >> c >> d;
if (op == 1) {
cin >> k;
add(a, b, k); // 不封装的话要写16个modify
add(c + 1, b, -k);
add(a, d + 1, -k);
add(c + 1, d + 1, k);
} else {
LL p1 = sum(c, d), p2 = sum(a - 1, d), p3 = sum(c, b - 1),
p4 = sum(a - 1, b - 1);
cout << p1 - p2 - p3 + p4 << '\n';
}
}
return 0;
}
树状数组的应用列举
树状数组只有单点修改和区间查询的操作,所以最大的作用是管理差分 、前缀和数据 ,以及权值树状数组作为计数器。
这里列举的很多问题都可以使用线段树解决。但这里的主角是树状数组,所以优先考虑树状数组。
P1908 逆序对 - 洛谷
逆序对可用归并排序、线段树、树状数组求解。这里使用树状数组。
原理和线段树一样,都是先离散化处理,然后每遍历一个数,就用和权值线段树一样功能的树状数组进行计数,同时查询逆序对的数量并统计。处理速度比归并排序慢,但比线段树快。
cpp
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;
struct Bitree { // 模拟权值线段树的树状数组
vl bt;
Bitree(int sz) {
bt.resize(sz + 2, 0); // 离散化后的数量可能等于原数组
}
inline int lowbit(int x) {
return x & -x;
}
void modify(int x) {
for (int i = x; i < bt.size(); i += lowbit(i))
bt[i]++;
}
LL query(int x) {
LL sum = 0;
for (int i = x; i; i -= lowbit(i))
sum += bt[i];
return sum;
}
};
int main() {
// freopen("in.in", "r", stdin);
int n;
cin >> n;
vl a(n + 1, 0), b;
for (int i = 1; i <= n; i++)
cin >> a[i];
// 备份
b = a;
// 离散化处理
sort(a.begin() + 1, a.end());
unordered_map<LL, int> ump; // ump[a[i]]=ip
for (int i = 1, ip = 0; i <= n; i++) {
if (ump.count(a[i]) > 0)
continue;
ump[a[i]] = ++ip;
}
// 权值树状数组求逆序对
Bitree bt(n);
LL ans = 0;
bt.modify(ump[b[1]]);
for (int i = 2; i <= n; i++) {
bt.modify(ump[b[i]]);
// sum(1,ip)-sum(1,ump[b[i]]),
// 即统计已被记录的大于b[i]的数的个数
ans += bt.query(ump.size()) - bt.query(ump[b[i]]);
}
cout << ans;
return 0;
}
P1966 火柴排队 - 洛谷
P1966 [NOIP 2013 提高组\] 火柴排队 - 洛谷](https://www.luogu.com.cn/problem/P1966)
> NOIP 的题目大都有一个特点,为结局题目需要解决隐含的各个问题,每个问题解决之后,整个题目就有眉目。
解决这个题,就需要解决 2 个问题:
1. **如何求得距离最小值**?
首先能想到的是贪心策略:当 a a a 、 b b b 均有序时,距离最小。
例如第 2 个测试样例 1 3 4 2 1 7 2 4 → 1 2 3 4 1 2 4 7 \\begin{matrix}1\&3\&4\&2\\\\1\&7\&2\&4\\end{matrix}\\rightarrow\\begin{matrix}1\&2\&3\&4\\\\1\&2\&4\&7\\end{matrix} 11374224→11223447 ,此时的距离最小。
之后用反证法证明这个贪心策略是否正确(正式比赛时间不够的话不用证):
贪心策略的结论是 ∑ ( a i − b i ) 2 \\sum(a_i-b_i)\^2 ∑(ai−bi)2 是最小距离,则设 a i \< a j , b i \< b j a_i\