16 KiB
树状数组
一、树状数组介绍
树状数组 是一种数据结构,它可以 快速计算前缀和 并 支持动态维护。与普通的前缀和数组相比,树状数组的 优势 在于它能够在O(logn)
的时间复杂度内 更新单个元素的值,同时仍然能够在O(logn)
的时间复杂度内 计算前缀和。这使得树状数组在 处理动态数据 时非常高效:
与普通前缀和区别
- 树状数组: 较快的修改时间
O(log_2N)
+较快的查询时间O(log_2N)
- 普通前缀和:较慢的修改时间
O(N)
+极快的查询时间O(1)
使用场景
-
单点更新与查询 树状数组支持在指定位置 更新元素,并且很快地查询更新后的结果。
- 例子:使用树状数组来计算数组
nums = [0, 0, 0, 0, 0]
中某个位置i
之前的所有元素和。首先执行单点更新操作update(2, 5)
,将位置2
的元素更新为5
。然后执行查询操作query(4)
,即查询位置4
之前的元素和,结果为0 + 0 + 5 + 0 = 5
。
- 例子:使用树状数组来计算数组
-
区间更新与查询 树状数组借助 差分思想 还可以 支持对指定区间内的元素进行更新,并且快速查询更新后的结果。
- 例子:使用树状数组来计算数组
nums = [0, 0, 0, 0, 0]
中某个区间[l, r]
内的所有元素和。首先执行区间更新操作rangeUpdate(2, 4, 2)
,即将位置2
到位置4
的元素都加2
,对应的差分数组:[0,2,0,0,-2]
,则从2
到4
都加上了2
,然后在第5
个位置将这个2
扣除掉,防止对后续带来影响。 - 注:理解的不是特别通透,需要完成复习后再回顾一下。
- 例子:使用树状数组来计算数组
-
数组逆序对统计 树状数组可以高效地计算数组中的逆序对个数。
- 例子:给定数组
nums = [5, 2, 6, 1, 3, 4]
,使用树状数组来统计逆序对的数量。逆序对是指数组中的一对元素(i, j)
,其中i < j
且nums[i] > nums[j]
。经过计算得到逆序对的数量为4
。 - 注:求逆序对,一般采用归并排序或者树状数组, 一般不使用线段树,常数太大。
- 例子:给定数组
-
维护最大、最小值 树状数组维护最大值的思路与维护前缀和类似。我们可以建立一个树状数组,其中每个节点存储它所代表的区间的最大值。在 更新 元素时,我们需要 沿着树状数组的路径向上更新 所有受影响的节点,以保证它们存储的最大值始终正确。在 查询 区间最大值时,我们可以将查询区间分解为若干个小区间,然后 查询这些小区间 在树状数组中对应节点的 最大值,最后 取所有查询结果的最大值 作为最终结果。
二、前置知识
lowbit()
运算:非负整数x
在二进制表示下最低位1
及其后面的0
构成的数值
举个栗子:
lowbit(12)=lowbit([1100]_2)=[100]_2=4
Code
int lowbit(int x){
return x & -x;
}
三、树状数组结构
树状数组的本质思想是使用 树结构 维护 前缀和 ,从而把时间复杂度降为O(log_2n)
。
① 每个节点t[x]
保存以x
为根的 子树中叶节点值的和
② 每个节点覆盖的长度 为lowbit(x)
③ t[x]
节点的父节点为t[x + lowbit(x)]
④ 树的深度为log_2n+1
四、树状数组操作
以add(3, 5)
为例:
在整棵树上维护这个值,需要一层一层向上找到父节点,并将这些节点上的c[x]
值都加上v
,这样保证计算区间和时的结果正确。时间复杂度为O(log_2n)
。
void add(int x, int v){
for(int i = x; i < N; i += lowbit(i)) c[i] += v;
}
以sum(7)
为例:
查询这个点的前缀和,需要从这个点向左上找到上一个节点,将加上其节点的值。向左上找到上一个节点,只需要将下标 x -= lowbit(x)
,例如 7 - lowbit(7) = 6
,6-lowbit(6)=4
,4-lowbit(4)=0
。
也就是sum(7)
只需要累加上图中黄色方框部分t[]
保存的区间和即可。
int sum(int x){
int sum = 0;
for(int i = x; i; i -= lowbit(i)) sum += c[i];
return sum;
}
五、题单
洛谷 P3374
【模板】树状数组 1
【基础模板,两个基础概念:位置+值】
HDU1166
敌兵布阵
【基础模板,两个基础概念:位置+值】
AcWing
788
. 逆序对的数量
【逆序对=树状数组+由小到大排序+去重离散化+二分+小的先进树状数组就找在当前元素右侧数字个数】
POJ
3067
Japan
【二维逆序对,结构体按一维由小到大,二维由小到大排序,不使用离散化】
P1966
[NOIP2013
提高组] 火柴排队
【逆序对,离散化,位置+高度,按高度由小到大排序,由小到大,逐个进入树状数组,找出所有先进入但位置在我右侧的元素数量】
POJ
2299
Ultra-QuickSort
【原地静态数组离散化,由小到大排序,配合lower\_bound
,比我小,并且序号在我后面的统计个数】
P2345
[USACO04OPEN
] 奶牛集会
【两个树状数组,一个用于维护奶牛的坐标和,一个用于维护奶牛前后的个数,数学分析式子】
P3368
【模板】树状数组 2
【区间修改,单点查询,树状数组维护差分,求和就是变化值,sum(k)+a[k]
】
AcWing
242
. 一个简单的整数问题
【区间修改,单点查询,树状数组维护差分,求和就是变化值,sum(k)+a[k]
】
LOJ
10117
「一本通 4.1
练习 2
」简单题
【区间修改,单点查询,树状数组维护差分,求和就是变化值,sum(k)+a[k]
】
LOJ
10115
. 「一本通 4.1
例 3
」校门外的树
【左右括号问题,两个树状数组,分别记录左括号个数,右括号个数】
AcWing
241
楼兰图腾
【树状数组+及时统计并用数组记录+动态单点修改】
POJ
2352
Stars
【扫描线+树状数组】
AcWing
243
. 一个简单的整数问题2
【区间修改、区间查询(利用差分+推公式)】
与上面是同一道题
P3372
【模板】线段树1
POJ
3468
A
Simple
Problem
with
Integers
注:其实【区间修改,区间查询】还得是线段树,用树状数组+推公式的办法也可以做,但不是正解
HDU
1754
I
Hate
It
【树状数组求最大最小值模板题】
POJ
3264
Balanced
Lineup
【树状数组求最大最小值,在上面的题目上同时加上求最大和求最小】
AcWing
244
. 谜一样的牛
【逆向思考+树状数组维护前缀和+二分快速查找sum=h[i]
】
HDU
2852
KiKi's
K-Number
【逆向思考+树状数组维护前缀和+二分快速查找】
二维树状数组
一、二维树状数组
二维树状数组,其实就是一维的树状数组上的节点再套个树状数组,变成了二维树状数组。
const int N = 1e3 + 10;
int c[N][N], n, m;
#define lowbit(x) (x & -x)
void add(int x, int y, int v) {
for (int i = x; i <= n; i += lowbit(i))
for (int j = y; j <= m; j += lowbit(j))
c[i][j] += v;
}
LL query(int x, int y) {
LL res = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
res += c[i][j];
return res;
}
二、单点修改,区间查询
给出一个
n × m
的零矩阵A
,你需要完成如下操作:
1
x
y
k
:表示元素A_{x , y}
增加k
2
a
b
c
d
: 表示询问左上角为(a,b)
,右下角为(c,d)
的子矩阵内所有数的和

单点增加,因此可以直接加上就可以了
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5000; // 2^(12)=4096
int n, m;
LL c[N][N];
#define lowbit(x) (x & -x)
void add(int x, int y, int d) {
for (int i = x; i < N; i += lowbit(i))
for (int j = y; j < N; j += lowbit(j))
c[i][j] += d;
}
LL sum(int x, int y) {
LL res = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
res += c[i][j];
return res;
}
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
int op;
while (cin >> op) {
if (op == 1) {
int x, y, d;
cin >> x >> y >> d;
add(x, y, d);
} else {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
cout << sum(x2, y2) - sum(x1 - 1, y2) - sum(x2, y1 - 1) + sum(x1 - 1, y1 - 1) << '\n';
}
}
return 0;
}
三、区间修改,单点查询
给出一个
n × m
的零矩阵A
,你需要完成如下操作:
1 \, a \, b \, c \, d \, k
:表示左上角为(a,b)
,右下角为(c,d)
的子矩阵内所有数都自增加k
;2 \, x \, y
:表示询问元素A_{x,y}
的值。
只需要利用一个二维树状数组,维护一个二维差分数组,单点查询即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5000;
int n, m;
LL c[N][N];
#define lowbit(x) (x & -x)
void add(int x, int y, int d) {
for (int i = x; i < N; i += lowbit(i))
for (int j = y; j < N; j += lowbit(j))
c[i][j] += d;
}
LL sum(int x, int y) {
LL res = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j))
res += c[i][j];
return res;
}
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
int op;
while (cin >> op) {
if (op == 1) {
int x1, y1, x2, y2, d;
cin >> x1 >> y1 >> x2 >> y2 >> d;
// 二维差分
add(x1, y1, d);
add(x1, y2 + 1, -d);
add(x2 + 1, y1, -d);
add(x2 + 1, y2 + 1, d);
} else {
int x, y;
cin >> x >> y;
cout << sum(x, y) << '\n';
}
}
return 0;
}
四、区间修改,区间查询
给定一个大小为
N × M
的零矩阵,直到输入文件结束,你需要进行若干个操作,操作有两类:
-
1 \, a\, b\, c\, d\, x
,表示将左上角为(a,b)
,右下角为(c,d)
的子矩阵全部加上x
; -
2\, a\, b\, c\, d\,
, 表示询问左上角为(a,b)
,右下角为(c,d)
为顶点的子矩阵的所有数字之和。
考虑前缀和 sum[i][j]
和 原数组 a
, 差分数组 d
之间的关系。
首先\displaystyle sum[i][j]=\sum_{x=1}^i\sum_{y=1}^ja[x][y]
(二维前缀和)
又由于\displaystyle a[x][y]=\sum_{u=1}^x\sum_{v=1}^yd[u][v]
(差分数组与原数组关系)
所以:
\displaystyle sum[i][j]=\sum_{x=1}^i\sum_{y=1}^j\sum_{u=1}^x\sum_{v=1}^yd[u][v]
可以说是非常复杂了......
统计d[u][v]
出现次数
-
从
a[1][1]
到a[i][j],d[1][1]
全都要出现一次,所以有i×j
个d[1][1]
,即d[1][1]×i×j
-
从
a[1][1]
到a[i][j]
,d[1][2]
出现了多少次呢?头脑中出现一个二维差分转原数组(本质就是一个原数组转二维前缀和)的图像:i=1,j=1
时,d[1][2]
就没有出现i=1,j=2
时,d[1][2]
出现1
次- ...
i=2,j=1
时,d[1][2]
就没有出现i=2,j=2
时,d[1][2]
出现1
次- ...
总结一下:
d[1][2]×i×(j−1)
d[2][1]×(i−1)×j
d[2][2]×(i−1)×(j−1)
等等……
所以我们不难把式子变成:
sum[i][j]=\sum_{x=1}^i\sum_{y=1}^j \begin{bmatrix}d[x][y]×(i+1−x)×(j+1−y)\end{bmatrix}
展开得到:
sum[i][j]=\sum_{x=1}^i\sum_{y=1}^j \begin{bmatrix}d[x][y]\times (i+1)\times (j+1)-d[x][y]\times x \times (j+1)-d[x][y]\times (i+1) \times y+d[x][y]\times xy \end{bmatrix}
也就相当于把这个式子拆成了四个部分: $\displaystyle ① (i+1)(j+1)×\sum_{x=1}^i\sum_{y=1}^jd[x][y] \ ② −(j+1)×\sum_{x=1}^i\sum_{y=1}^j(d[x][y]⋅x) \ ③ −(i+1)×\sum_{x=1}^i\sum_{y=1}^j(d[x][y]⋅y) \ ④ \sum_{x=1}^i\sum_{y=1}^j(d[x][y]⋅xy)$
所以我们需要在原来 C_1[i][j]
记录 d[i][j]
的基础上,再添加三个树状数组:
C_2[i][j]
记录 d[i][j]⋅i
C_3[i][j]
记录 d[i][j]⋅j
C_4[i][j
] 记录 d[i][j]⋅ij
这样一来,就能通过数组a[i][j]
的差分数组d[i][j]
来得到a[i][j]
的前缀和数组sum[i][j]
。
最后,易知(x_1,y_1)
到(x_2,y_2)
的矩阵和就是一个标准的二维前缀和公式,等于\large sum[x_2][y_2]−sum[x_2][y_1−1]−sum[x_1−1][y_2]+sum[x_1−1][y_1−1]
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2050;
int n, m;
LL c1[N][N], c2[N][N], c3[N][N], c4[N][N];
#define lowbit(x) (x & -x)
// 维护四个树状数组
void add(int x, int y, int v) {
for (int i = x; i < N; i += lowbit(i))
for (int j = y; j < N; j += lowbit(j)) {
c1[i][j] += v;
c2[i][j] += v * x;
c3[i][j] += v * y;
c4[i][j] += v * x * y;
}
}
// 查询左上角为(1,1)右下角为(x,y)的矩阵和
LL query(int x, int y) {
LL res = 0;
for (int i = x; i; i -= lowbit(i)) {
for (int j = y; j; j -= lowbit(j)) {
res += (x + 1) * (y + 1) * c1[i][j];
res -= (y + 1) * c2[i][j];
res -= (x + 1) * c3[i][j];
res += c4[i][j];
}
}
return res;
}
int main() {
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
int op;
while (cin >> op) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
if (op == 1) {
int d;
cin >> d;
// 维护四个数组
add(x1, y1, d);
add(x1, y2 + 1, -d);
add(x2 + 1, y1, -d);
add(x2 + 1, y2 + 1, d);
} else
cout << query(x2, y2) - query(x1 - 1, y2) - query(x2, y1 - 1) + query(x1 - 1, y1 - 1) << '\n';
}
return 0;
}
POJ
2155
Matrix
【二维树状数组】