0%

前缀和与差分

前缀和

前缀和(Prefix Sum)是一个非常常见且高效的算法技巧,用来快速计算数组或矩阵中某个区间的元素和。它的核心思想是通过预处理数组(或矩阵),构建一个新的数组(或矩阵),使得可以在常数时间内计算任意区间的和,从而显著提高效率。

原理

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
#include <iostream>
using namespace std;

using ll = long long;

const ll N = 1e5 + 9;
ll arr[N] = {0};
ll prefix[N] = {0};

int main()
{
int n;
cin >> n;

// 输入数组并计算前缀和
for (int i = 1; i <= n; i++) {
cin >> arr[i];
prefix[i] = prefix[i - 1] + arr[i];
}

int m;
cin >> m;

// 处理每个查询区间 [l, r]
while (m--) {
int l, r;
cin >> l >> r;

cout << prefix[r] - prefix[l - 1] << endl;
}

return 0;
}

二维前缀和

目的是预处理出一个结构,以后每次查询二维数组任何范围上的累加和都是O(1)的操作
1 根据原始状况,生成二维前缀和数组sum,
sum[i][j]: 代表左上角(0,0)到右下角(i,j)这个范围的累加和
sum[i][j] += sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1];
2 查询左上角(a,b)到右下角(c,d)这个范围的累加和
sum[c][d] - sum[c][b-1] - sum[a-1][d] + sum[a-1][b-1];
3 实际过程中往往补第0行、第0列来减少很多条件判断。
当然也可以不补。根据个人习惯决定

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
#include <iostream>
#include <cstdio>
using namespace std;

using ll = long long;

const ll N = 1e3; // 定义二维数组的最大尺寸
ll arr[N][N] = {0}; // 原始数组
ll prefix[N][N] = {0}; // 二维前缀和数组

int main()
{
// 输入矩阵的大小
int n;
cin >> n;

// 输入子矩阵查询的坐标 (x1, y1) 到 (x2, y2)
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;

// 输入原始数组并计算二维前缀和
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> arr[i][j];
// 计算二维前缀和
prefix[i][j] = prefix[i - 1][j] + prefix[i][j - 1] - prefix[i - 1][j - 1] + arr[i][j];
}
}

// 输出原始数组
cout << "原数组:" << endl;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("%4lld ", arr[i][j]);
}
cout << endl;
}

// 输出二维前缀和
cout << "二维前缀和:" << endl;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("%4lld ", prefix[i][j]);
}
cout << endl;
}

// 输出指定子矩阵的和,并附加查询的区间
ll submatrix_sum = prefix[x2][y2] - prefix[x1 - 1][y2] - prefix[x2][y1 - 1] + prefix[x1 - 1][y1 - 1];
cout << "查询区域: (" << x1 << ", " << y1 << ") 到 (" << x2 << ", " << y2 << ")" << endl;
cout << "子矩阵的和: " << submatrix_sum << endl;

return 0;
}

差分

差分(Difference)是一种常见的算法技巧,通常用于处理数组或区间更新问题。差分的基本思想是通过对数组的差分(即存储相邻元素的差值)来高效地进行区间修改或区间查询。

原理

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
#include <iostream>
using namespace std;

using ll = long long;
const ll N = 1e5 + 9;
ll arr[N] = {0};
ll diff[N] = {0};
ll prefix[N] = {0};

int main() {
int n;
cin >> n;

// 输入原数组
for (int i = 1; i <= n; i++) {
cin >> arr[i];
}

// 构建差分数组
for (int i = 1; i <= n; i++) {
diff[i] = arr[i] - arr[i - 1];
}

// 输出初始差分数组
cout << "差分数组:\n";
for (int i = 1; i <= n; i++) {
cout << diff[i] << " ";
}
cout << endl;

// 区间操作:对 [l, r] 加 1
int l, r;
cin >> l >> r;
diff[l] += 1;
diff[r + 1] -= 1;

// 通过前缀和还原数组
for (int i = 1; i <= n; i++) {
prefix[i] = prefix[i - 1] + diff[i];
}

// 输出还原后的数组
cout << "还原后的数组:\n";
for (int i = 1; i <= n; i++) {
cout << prefix[i] << " ";
}
cout << endl;

return 0;
}

二维差分

在二维数组中,如果经历如下的过程
1 批量的做如下的操作,每个操作都有独立的a、b、c、d、v
void add(a, b, c, d, v) : 左上角(a,b)到右下角(c,d)范围上,每个数字+v,怎么快速处理?
2 操作做完后,如何正确得到二维数组中每个位置的值?

这就是二维差分的主要工作,add时候快速处理,最后build得到每个位置的值,修改操作必须集中在一起,不能边修改边查询。
1)add方法实现,比较巧妙!
2)build方法实现,和处理前缀和类似
3)真实数据用一圈0包裹起来,可以减少很多边界讨论

1
2
3
4
5
6
7
void build() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];
}
}
}
1
2
3
4
5
6
void add(int a, int b, int c, int d, int v) {
diff[a][b] += v;
diff[c + 1][b] -= v;
diff[a][d + 1] -= v;
diff[c + 1][d + 1] += v;
}
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
#include <iostream>
using namespace std;

using ll = long long;
ll arr[1009][1009] = {0};
ll diff[1009][1009] = {0};
ll sum[1009][1009] = {0};

int main()
{
ios::sync_with_stdio(false);
cin.tie(NULL);
int n, m, q;
cin >> n >> m >> q;

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> arr[i][j];
//diff[i][j] 表示 (1, 1) 到 (i, j) 区域的差分
diff[i][j] = arr[i][j] - arr[i - 1][j] - arr[i][j - 1] + arr[i - 1][j - 1];
}
}

while (q--) {
int x1, y1, x2, y2, k;
cin >> x1 >> y1 >> x2 >> y2 >> k;
diff[x1][y1] += k;
diff[x1][y2 + 1] -= k;
diff[x2 + 1][y1] -= k;
diff[x2 + 1][y2 + 1] += k;
}

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + diff[i][j];
cout << sum[i][j] << " ";
}
cout << endl;
}

return 0;
}

https://www.luogu.com.cn/problem/P7404

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
#include <bits/stdc++.h>
#define int long long
using namespace std;

int n, cnt;
const int N = 2e5 + 10;
int arr[N];
int diff[N];

void solve()
{
cin >> n;
// 这道题等价于让差分数组 + + + + ... + - ... - - - -
for (int i = 1; i <= n; i++) {
cin >> arr[i];
diff[i] = arr[i] - arr[i - 1];
}
int l = 1, r = n;
while (l < r) {
while (diff[l] > 0) {
// 如果是递增的,那就向l++
// 最后l是第一个不是递增的数
l++;
}
while (diff[r] < 0) {
// 如果是递减的,那就r--
// 最后r是第一个不是递减的数
r--;
}
if (l > r) {
break;
}
int x = min(abs(diff[l]), diff[r]) + 1;
// 让区间里面的数增加,实际上可以看作对于差分数组的修改
// 我们每次只需让修改次数最少的一边保持单调性即可
// 局部最优 --> 全局最优
diff[l] += x;
diff[r] -= x;
cnt += x;
}
cout << cnt << endl;

}

signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t = 1;
// cin >> t;
while(t--){
solve();
}

return 0;
}