一、前缀和(Prefix Sum)
前缀和的核心思想是预先计算并存储累积信息,将区间求和操作从 O(n) 优化到 O(1),特别适合需要频繁查询子数组或子矩阵和的场景。适用于需要频繁计算 “子数组 / 子矩阵和” 的场景。
1. 一维前缀和
定义:对于数组a[1..n]
(建议从 1 开始索引,避免边界判断),其前缀和数组s[1..n]
满足:
s[i] = a[1] + a[2] + ... + a[i]
(即前 i 个元素的累加和)。
计算公式:
通过 “递推” 快速构造前缀和数组:
s[0] = 0
s[i] = s[i-1] + a[i]
(i≥1)
核心应用:快速求子数组和
若需计算a[l..r]
(从第 l 个到第 r 个元素)的和,直接用前缀和相减:
sum(l, r) = s[r] - s[l-1]
举例:
设a = [1, 2, 3, 4, 5]
(索引 1-5),则前缀和数组s
为:
s[0]=0,s[1]=1,s[2]=3(1+2),s[3]=6(1+2+3),s[4]=10,s[5]=15
求a[2..4]
的和:s[4] - s[1] = 10 - 1 = 9
。
2. 二维前缀和(矩阵前缀和)
定义:对于二维数组(矩阵)a[1..n][1..m]
,其前缀和数组s[1..n][1..m]
满足:
s[i][j] = 所有a[x][y]的和(其中x≤i,y≤j)
(即左上角 (1,1) 到右下角 (i,j) 的矩形内所有元素和)。
计算公式(建议不要死记,画图+容斥原理推导):
需避免重复累加(s[i-1][j]
和s[i][j-1]
均包含s[i-1][j-1]
),公式为:
s[i][j] = a[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1]
(s[0][*] = 0
,s[*][0] = 0
,辅助边界)
核心应用:快速求子矩阵和
若需计算 “左上角 (x1,y1) 到右下角 (x2,y2)” 的子矩阵和,公式为:
sum(x1,y1,x2,y2) = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]
举例:
设 3x3 矩阵a
如下(索引 1-3):
1 2 3
4 5 6
7 8 9
其前缀和s[2][2]
(对应 (1,1)-(2,2) 的矩形)计算:
s[2][2] = 5 + s[1][2](1+2=3) + s[2][1](1+4=5) - s[1][1](1) = 5+3+5-1=12
求 (2,2)-(3,3) 的子矩阵和(5+6+8+9=28):
s[3][3](45) - s[1][3](6) - s[3][1](12) + s[1][1](1) = 45-6-12+1=28
。
二、差分(Difference)
差分的核心是通过 “差分数组” 记录 “原数组的变化量”,将 “区间更新” 从 O (n) 优化到 O (1)。适用于需要频繁对 “区间元素做统一加减” 的场景。
1. 一维差分
定义:对于数组a[1..n]
,其差分数组d[1..n]
满足:a 是 d 的前缀和。即:
d[1] = a[1]
d[i] = a[i] - a[i-1]
(i≥2)
反过来,若已知d
,通过 “前缀和” 可还原a
:
a[i] = d[1] + d[2] + ... + d[i]
(即 d 的前缀和为 a)。
核心应用:快速区间更新
若需对a[l..r]
的所有元素 “加 k”(或减 k),无需逐个修改 a,只需操作差分数组 d:
d[l] += k
(从 l 开始,所有 a [i](i≥l)都会因 d 的前缀和增加 k)d[r+1] -= k
(从 r+1 开始,抵消 k 的影响,避免 a [i](i>r)被误改)
最后通过 “d 的前缀和” 即可得到更新后的 a 数组。
举例:
原数组a = [1, 2, 3, 4, 5]
,差分数组d
为:
d[1]=1,d[2]=2-1=1,d[3]=3-2=1,d[4]=4-3=1,d[5]=5-4=1
(d 全为 1,符合 a 是 d 的前缀和:1,1+1=2,1+1+1=3...)。
需求:给a[2..4]
的元素各加 2(目标 a 变为 [1,4,5,6,5])。
操作 d:
d[2] += 2
→ d [2] 变为 3d[5] -= 2
(因 r=4,r+1=5)→ d [5] 变为 - 1
此时 d 为[1,3,1,1,-1]
,求 d 的前缀和还原 a:
a [1]=1;a [2]=1+3=4;a [3]=1+3+1=5;a [4]=1+3+1+1=6;a [5]=1+3+1+1+(-1)=5(与目标一致)。
2. 二维差分
定义:对于矩阵a[1..n][1..m]
,其差分数组d[1..n][1..m]
满足:a 是 d 的二维前缀和。
核心应用:快速子矩阵更新
若需对 “左上角 (x1,y1) 到右下角 (x2,y2)” 的所有元素 “加 k”,只需操作 d 的 4 个角落:
还是建议画图+容斥原理
d[x1][y1] += k
(起点加 k,影响整个子矩阵)d[x1][y2+1] -= k
(右边界减 k,抵消子矩阵外右侧的影响)d[x2+1][y1] -= k
(下边界减 k,抵消子矩阵外下方的影响)d[x2+1][y2+1] += k
(右下角补 k,抵消重复减去的部分)
最后通过 “d 的二维前缀和” 即可得到更新后的 a 矩阵。
举例:
原 3x3 矩阵a
全为 0(d 初始也全为 0),需求:给 (1,1)-(2,2) 的子矩阵各加 2(目标子矩阵元素为 2)。
操作 d:
d[1][1] += 2
,d[1][3] -= 2
(y2+1=2+1=3),d[3][1] -= 2
(x2+1=2+1=3),d[3][3] += 2
。
此时 d 的关键位置:d [1][1]=2,d [1][3]=-2,d [3][1]=-2,d [3][3]=2。
求 d 的二维前缀和还原 a:
a [1][1]=2(正确),a [1][2]=2(受 d [1][1] 影响),a [2][2]=2(正确);子矩阵外的 a [3][3] 经计算仍为 0(无影响)。
三、前缀和与差分的关系
二者是互逆操作:
- 对数组
a
求差分得到d
,再对d
求前缀和,结果是a
; - 对数组
a
求前缀和得到s
,再对s
求差分,结果是a
。
简单说:差分是 “前缀和的逆运算”,前缀和是 “差分的正运算”。
四、适用场景总结
技巧 | 核心作用 | 典型场景 | 时间复杂度优化 |
---|---|---|---|
一维前缀和 | 快速求子数组和 | 频繁查询 “区间和”(如统计子数组和) | 查询从 O (n)→O (1) |
二维前缀和 | 快速求子矩阵和 | 频繁查询 “子矩阵和”(如矩阵区域和) | 查询从 O (nm)→O (1) |
一维差分 | 快速对区间元素统一加减 | 频繁更新 “区间元素”(如区间加 k) | 单次更新从 O (n)→O (1) |
二维差分 | 快速对子矩阵元素统一加减 | 频繁更新 “子矩阵元素”(如矩阵区域加 k) | 单次更新从 O (nm)→O (1) |
通过前缀和与差分的预处理,能将原本需要 “暴力遍历” 的操作转化为 “常数级” 的公式计算,是算法题中解决数组 / 矩阵问题的常用方法。
附一道练习题:
解决方法:
1 暴力
#include<iostream>
#include<vector>
#include <cmath>
#include <string>
#include <climits>
using namespace std;
int main() {
int n, m;
int suma=0, sumb=0;
int diff = INT_MAX;
cin >> n >> m;
vector<vector<int>>lands(n, vector<int>(m));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> lands[i][j];
}
}
int total = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
total += lands[i][j];
}
}
//横着划分
for (int k = 0; k < n-1; k++) {
for (int i = 0; i <= k; i++) {
for (int j = 0; j < m; j++) {
suma += lands[i][j];
}
}
sumb = total - suma;
diff = abs(suma - sumb) > diff ? diff : abs(suma - sumb);
suma = 0, sumb = 0;
}
//竖着划分
for (int k = 0; k < m-1; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j <= k; j++) {
suma += lands[i][j];
}
}
sumb = total - suma;
diff = abs(suma - sumb) > diff ? diff : abs(suma - sumb);
suma = 0, sumb = 0;
}
cout << diff;
}
2. 前缀和
#include<iostream>
#include<vector>
#include <cmath>
#include <string>
#include <climits>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>>lands(n + 1, vector<int>(m + 1,0));
vector<vector<int>>sum(n + 1, vector<int>(m + 1,0));
int diff = INT_MAX;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> lands[i][j];
}
}
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] + lands[i][j];
}
}
int total = sum[n][m];
//横着遍历
for (int k = 1; k < n; k++) {
diff = min(diff, abs(2 * sum[k][m] - total));
}
//竖着遍历
for (int k = 1; k < m; k++) {
diff = min(diff, abs(2 * sum[n][k] - total));
}
cout << diff;
}