前缀和
用途
多用于少量修改,多次查询的问题场景中,单词查询的时间复杂度是 O ( 1 ) \mathcal O(1) O(1) 的,但是如果修改了一个元素,那么这个元素后面的前缀和都将受到影响,最劣情况下需要遍历整个前缀和数组,时间复杂度是 O ( n ) \mathcal O(n) O(n) 的。在这片博文中,我们用 a a a 代表原数组,用 s s s 代表前缀和数组。
由于 s 0 s_0 s0 是特殊定义为 0 0 0 的,所以我们将下标从 1 1 1 可以计算。
一维前缀和
理论实现
因为元素 s i s_i si 的值是 ∑ x = 1 i a i \sum\limits_{x=1}^{i} a_i x=1∑iai,所以,我们可以求 s i s_i si 时遍历 a 1 ∼ a i a_1\sim a_i a1∼ai,这样就可以得到 s i s_i si 的值。时间复杂度是 O ( n 2 ) \mathcal O(n^2) O(n2) 的。虽然这种做法对于小范围的数据可以通过,但是一旦 n n n 达到了 1 0 5 10^5 105,这种方法是承受不住的,所以我们需要更加快的方法。
思考一下,我们需要遍历 a 1 ∼ a i − 1 a_1\sim a_{i-1} a1∼ai−1 吗?我们可以发现, ∑ x = 1 i − 1 a i \sum\limits_{x=1}^{i-1} a_i x=1∑i−1ai 的值已经存储在 s i − 1 s_{i-1} si−1 里了!所以,我们无需遍历 a 1 ∼ a i − 1 a_1\sim a_{i-1} a1∼ai−1。所以,一维前缀和的通项公式是: s i ← s i − 1 + a i s_i\gets s_{i-1}+a_i si←si−1+ai
程序实现
清楚了一维前缀和的理论实现,程序实现也不难了。
// 一维前缀和程序实现
const int MAXN = 1e5 + 10;
int N, a[MAXN], s[MAXN]; // N是数组的长度
int main() {
cin >> N;
for (int i = 1; i <= N; i++)
cin >> a[i], s[i] = s[i - 1] + a[i];
return 0;
}
经过这份程序,我们就已经将 a a a 数组的前缀和存储在 s s s 数组中了。
查询
查到头
这种方法的意思是查找 ∑ x = 1 x a x \sum\limits_{x=1}^{x}a_x x=1∑xax,就是要求 a 1 ∼ a i a_1\sim a_i a1∼ai 的和。这样是最简单的,只需输出 s i s_i si 即可,因为 s i s_i si 存储的就是这个值。
程序实现:
int index;
cin >> index;
cout << s[index] << endl;
中间字串查找
这种方法的意思是查找 ∑ x = l r a x \sum\limits_{x=l}^{r}a_x x=l∑rax,就是求 a l ∼ a r a_l\sim a_r al∼ar 的和。这一点需要注意的是,答案是 a r − a l − 1 a_r-a_{l-1} ar−al−1!这很容易证明,如果答案是 a r − a l − 1 a_r-a_{l-1} ar−al−1,那么求的是 ( l , r ] (l,r] (l,r] 这一区间的和,并不包括 a l a_l al,所以这是错误的。
程序实现:
int l, r;
cin >> l >> r;
cout << s[r] - s[l - 1] << endl;
差分
用途
多用于多次修改,少量查询的问题场景中,单词修改的时间复杂度是 O ( 1 ) \mathcal O(1) O(1) 的,但是如果要查询一个元素,那么我们就需要计算一遍整个差分数组的前缀和,时间复杂度是 O ( n ) \mathcal O(n) O(n) 的。在这片博文中,我们用 a a a 代表原数组,用 b b b 代表前缀和数组。
由于 b 0 b_0 b0 是特殊定义为 0 0 0 的,所以我们将下标从 1 1 1 可以计算。
二维差分不会写,所以就不放了
理论实现
我们可以定
b
1
←
a
1
b_1\gets a_1
b1←a1,
b
i
←
a
i
−
a
i
−
1
(
2
≤
i
≤
n
)
b_i\gets a_i-a_{i-1}(2\le i\le n)
bi←ai−ai−1(2≤i≤n)。例如:
i
1
2
3
4
5
6
7
8
a
i
1
1
2
2
1
3
3
5
b
i
1
0
1
0
−
1
2
0
2
\begin{array}{|c|c|c|c|c|c|c|c|c|} \hline i & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8\\\hline a_i&1&1&2&2&1&3&3&5\\\hline b_i&1&0&1&0&-1&2&0&2\\\hline \end{array}
iaibi11121032142051−1632730852
对数组
b
b
b 求前缀和:
∑
i
=
1
n
b
i
=
a
1
+
∑
i
=
2
n
(
a
i
−
a
i
−
1
)
=
∑
i
=
1
n
a
i
−
∑
i
=
1
n
−
1
a
i
=
a
n
\sum\limits_{i=1}^{n}b_i=a_1+\sum\limits_{i=2}^{n}(a_i-a_{i-1})=\sum\limits_{i=1}^{n}a_i-\sum\limits_{i=1}^{n-1}a_i=a_n
i=1∑nbi=a1+i=2∑n(ai−ai−1)=i=1∑nai−i=1∑n−1ai=an
可以发现,
b
b
b 的前缀和数组
s
s
s 的
s
i
s_i
si 恰好等于
a
i
a_i
ai!
若将
b
x
b_x
bx 增加
1
1
1,再对
b
b
b 数组求前缀和得到
a
a
a 数组,那么得到的
a
a
a 数组中的
a
x
,
a
x
+
1
,
⋯
,
a
n
a_x,a_{x+1},\cdots,a_n
ax,ax+1,⋯,an 均增加
1
1
1。若将
b
y
b_y
by 减少
1
1
1,再对
b
b
b 数组求前缀和得到
a
a
a 数组,那么得到的
a
a
a 数组中
a
y
,
a
y
+
1
,
⋯
,
a
n
a_y,a_{y+1},\cdots,a_n
ay,ay+1,⋯,an 均减少
1
1
1。
例如,对一个全为
0
0
0 的差分数组
b
b
b,将
b
3
b_3
b3 增加
1
1
1,将
b
6
b_6
b6 减少
1
1
1,得到的
a
a
a 数组如表所示:
i
1
2
3
4
5
6
7
8
b
i
0
0
+
1
0
0
−
1
0
0
a
i
0
0
1
1
1
0
0
0
\begin{array}{|c|c|c|c|c|c|c|c|c|} \hline i&1&2&3&4&5&6&7&8\\\hline b_i&0&0&+1&0&0&-1&0&0\\\hline a_i&0&0&1&1&1&0&0&0\\\hline \end{array}
ibiai1002003+114015016−10700800
因此,将 a a a 数组中第 x x x 到第 y y y 个元素增加 z z z 这样的修改操作,可以转化为将 b x b_x bx 增加 z z z,将 b y + 1 b_{y+1} by+1 减少 z z z,最后再对 b b b 数组求前缀和。这样,对于每次修改操作,只需要修改两个值,时间复杂度也就是 O ( 1 ) \mathcal O(1) O(1) 的了。