树状数组
- 基本原理
- 扩展
- 差分
- 查分 + 推公式
- 例题: 哪些题型可以使用树状数组
题型:
一般遇见这两类题型,或者“二合一”的问法,就说明需要树状数组了 ⚠️
- 快速求前缀和
O(logN)
: \(\sum a[L,R]\) - 修改某一个数
O(logN)
:a[x] += C
树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构
事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的代码要远比线段树短,时间效率常数也更小,因此仍有学习价值。
有时,在差分数组和辅助数组的帮助下,树状数组还可解决更强的 区间加单点值 和 区间加区间和 问题。
基本原理¶
过程1小时,代码1分钟
模板:
C++ | |
---|---|
1 2 3 4 5 6 7 8 |
|
(1) 前置知识lowbit(x)
定义: 非负整数x在二进制表示下 最低位1及其后面的0 构成的 数值
C++ | |
---|---|
1 2 3 |
|
(2) add(x, k)
表示将序列中第 x 个数加上 k
在整棵树上维护这个值,需要一层一层向上找到父结点,并将这些结点上的
t[x]
值都加上k
C++ | |
---|---|
1 2 3 4 |
|
(3) ask(x)
表示将查询序列前 x 个数的和
查询这个点的前缀和,需要从这个点向左上找到上一个结点,将加上其结点的值。向左上找到上一个结点,只需要将下标 x -= lowbit(x),例如 7 - lowbit(7) = 6
C++ | |
---|---|
1 2 3 4 5 6 |
|
(4) 让谁成为“树状数组”
本质上, 是想让
idx=i
的位置等于谁 ("我们的前缀和究竟是想维护谁") 🌟
C++ | |
---|---|
1 2 3 4 5 |
|
241 楼兰图腾¶
这题非常不适合入门,建议先跳过,看 242 and 243
(1) 朴素的想法 (O(N³))
最直接的想法是枚举所有可能的三个点 (i, y_i), (j, y_j), (k, y_k)
且 i < j < k
,然后判断它们是否构成 V 或 ∧ 图腾。这需要三层循环,时间复杂度是 O(N³),对于 N = 200000
来说是绝对无法通过的。
(2) 优化:固定中间点 (O(N²))
设计思路:
与其枚举三个点,不如我们枚举中间那个点 j
,然后统计它能和左边、右边的多少个点组成图腾
-
对于一个固定的中间点
(j, y_j)
:-
要形成一个 V 图腾 (
y_i > y_j < y_k
),我们需要在它左边(i < j
)找一个点(i, y_i)
使得y_i > y_j
,同时在它右边(k > j
)找一个点(k, y_k)
使得y_k > y_j
-
假设点
j
左边有L_greater
个点的y
值比y_j
大 -
假设点
j
右边有R_greater
个点的y
值比y_j
大 -
根据乘法原理,以
j
为谷底的 V 图腾就有L_greater * R_greater
个
-
-
要形成一个 ∧ 图腾 (
y_i < y_j > y_k
),我们需要在它左边(i < j
)找一个点(i, y_i)
使得y_i < y_j
,同时在它右边(k > j
)找一个点(k, y_k)
使得y_k < y_j
-
假设点
j
左边有L_lower
个点的y
值比y_j
小 -
假设点
j
右边有R_lower
个点的y
值比y_j
小 -
根据乘法原理,以
j
为山峰的 ∧ 图腾就有L_lower * R_lower
个
-
-
-
总数:
-
总的 V 图腾数 = ∑j=1n(L_greaterj×R_greaterj)
-
总的 ∧ 图腾数 = ∑j=1n(L_lowerj×R_lowerj)
-
算法的整体流程设计得非常巧妙:
-
第一次遍历(从左到右):
-
遍历
i
从 1 到n
。 -
对于每个
a[i]
,我们利用树状数组快速查询在它左边(也就是a[1]
到a[i-1]
中)有多少个数比a[i]
小,有多少个数比a[i]
大。 -
将这些结果存起来。
Lower[i]
存左边比a[i]
小的数的个数,Greater[i]
存左边比a[i]
大的数的个数。 -
查询完后,把
a[i]
这个数“加入”到树状数组中,供后面的元素查询。
-
-
第二次遍历(从右到左):
-
清空树状数组。
-
遍历
i
从n
到 1。 -
对于每个
a[i]
,我们利用树状数组快速查询在它右边(也就是a[i+1]
到a[n]
中,因为是逆序遍历,这些都是已经处理过的)有多少个数比a[i]
小,有多少个数比a[i]
大。 -
在这一步,我们不需要把右边的计数值存起来。因为当我们处理到
a[i]
时,我们已经从第一次遍历中知道了Lower[i]
和Greater[i]
(左边的计数值)。我们可以直接计算a[i]
作为中间点的贡献:-
V 图腾贡献:
Greater[i] * (右边比 a[i] 大的个数)
-
∧ 图腾贡献:
Lower[i] * (右边比 a[i] 小的个数)
-
-
将这些贡献累加到总和中。
-
查询和计算贡献后,把
a[i]
这个数“加入”到树状数组中,供更左边的元素查询。
-
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
|
242 一个简单的整数问题¶
给定长度为 N 的数列 A,然后输入 M 行操作指令
第一类指令形如 C l r d
,表示把数列中第 l∼r 个数都加 d
第二类指令形如 Q x
,表示询问数列中第 x 个数的值
对于每个询问,输出一个整数表示答案
本质: 前缀和 -> 差分
Review: 差分数组
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
243 一个简单的整数问题 2¶
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d
,表示把A[l], A[l+1] , … , A[r]
都加上 dQ l r
,表示询问数列中第l∼r
个数的和
对于每个询问,输出一个整数表示答案
分析:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
|
244 谜一样的牛¶
USACO, 略