一.一维前缀和
一、核心概念
定义
前缀和是一种预处理技术,通过构建数组prefix[],其中prefix[i]表示原数组arr前i个元素的和。例如:
原数组:[1, 3, 5, 7, 9]
前缀和数组:[1, 4, 9, 16, 25](prefix[2] = 1+3 = 4)
数学表达
递推公式:prefix[i] = prefix[i-1] + arr[i-1](下标从1开始)
区间和计算:sum[l, r] = prefix[r] - prefix[l-1]
二、实现步骤
预处理前缀和数组
int prefix[n+1];
for (int i = 1; i <= n; i++) {
prefix[i] = prefix[i-1] + arr[i-1];
}
时间复杂度:O(n)
空间复杂度:O(n)
查询区间和
int query(int l, int r) {
return prefix[r] - prefix[l-1];
}
时间复杂度:O(1)
三、应用场景
-
高频区间求和:如统计数组任意子数组的和,避免重复计算。
-
差分数组结合:通过前缀和还原差分操作后的数组,用于区间批量增减。
-
滑动窗口优化:快速计算窗口内元素和,例如求长度为k的子数组最大和。
四、边界处理技巧
下标从1开始
定义prefix[0]=0,避免l=0时的越界问题。
负数与溢出
使用long long存储前缀和防止溢出。
五、经典例题
基础应用
题目:给定数组和多个查询[l, r],输出每个查询的区间和。
解法:直接套用前缀和模板。
扩展问题
统计满足条件的子数组数量(如和为k的倍数)。
六、对比暴力法
方法 | 预处理时间 | 查询时间 | 适用场景 |
暴力遍历 | 无 | O(n) | 单次查询 |
前缀和 | O(n) | O(1) | 高频区间查询 |
代码
#include<bits/stdc++.h> //万能头文件
using namespace std;
int main(){
int n,p; //数组长度为n,询问p次
cin>>n>>p;
int a[n+1];
int sum[n+1]={0};
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i]; //预处理
}
while(p--){ //询问p次
int l;
int r;
cin>>l>>r;
cout<<sum[r]-sum[l-1]<<endl; //a[l]至a[r]的和
}
return 0;
}
小结
通过预处理将区间查询优化为常数时间,前缀和是处理线性数据求和问题的核心工具。
二.一维差分
一维差分是处理数组区间修改的高效算法,核心思想是通过维护差分数组将区间操作优化为单点操作。
一、基本定义
差分数组构造
给定原数组 a[1..n],其差分数组 d[] 定义为:
d[1] = a[1](假设 a[0]=0)
d[i] = a[i] - a[i-1](i ≥ 2)
此时,原数组 a[i] 是 d[] 的前缀和:
a[i] = d[1] + d[2] + ... + d[i]
差分与前缀和的互逆关系
差分是前缀和的逆运算:对差分数组求前缀和可还原原数组。
二、核心操作:区间修改
若需将原数组区间 [L, R] 的所有元素值增加 c,只需修改差分数组的两个端点:
d[L] += c
d[R+1] -= c(若 R+1 > n 则忽略)
原理:
d[L] += c 使得 a[L] 及后续所有元素前缀和增加 c;
d[R+1] -= c 抵消了 a[R+1] 及后续元素的额外增量。
时间复杂度:区间修改仅需 O(1)。
操作示例
假设原数组 a = [3, 5, 2, 7],差分数组 d = [3, 2, -3, 5]。
将区间 [1, 3](下标从1开始)加 4:
d[1] += 4 → d = [7, 2, -3, 5]
d[4] -= 4 → d = [7, 2, -3, 1]
还原原数组(前缀和):
a[1]=7, a[2]=7+2=9, a[3]=9+(-3)=6, a[4]=6+1=7 → 结果 [7, 9, 6, 7]
三、实现步骤
初始化差分数组
// 原数组 a[], 差分数组 d[]
d[1] = a[1];
for (int i = 2; i <= n; i++) {
d[i] = a[i] - a[i-1];
}
执行区间修改
void add(int L, int R, int c) {
d[L] += c;
if (R + 1 <= n) d[R+1] -= c; // 边界检查
}
还原原数组(前缀和)
for (int i = 2; i <= n; i++) {
d[i] += d[i-1]; // d[i] 变为修改后的 a[i]
}
// 此时 d[] 即为更新后的原数组
四、应用场景
高效处理区间更新:如批量增减、多次区间操作(如 m 次操作总复杂度 O(m + n))。
单点查询辅助:修改后通过前缀和快速获取任意位置值。
五、常见问题
下标起点:通常从 1 开始,避免边界特判(d[0] 无意义)。
越界处理:若 R+1 > n,跳过 d[R+1] 的修改。
代码
#include<bits/stdc++.h> //万能头文件
using namespace std;
int main(){
int n,p; //数组长度为n,询问p次
cin>>n>>p;
int a[n+1]={0};
int dif[n+1]={0};
for(int i=1;i<=n;i++){
cin>>a[i];
dif[i]=a[i]-a[i-1]; //预处理
}
while(p--){
int l;
int r;
int k;
cin>>l>>r>>k;
dif[l]+=k; ////a[l]与a[l-1]的差发生改变
if(r!=n)dif[r+1]-=k; //特判:最后一个会超界,否则会RE
}
//还原数组:差分数组的前缀和数组是原数组
int now=0;
for(int i=1;i<=n;i++){
now+=dif[i];
cout<<now<<" ";
}
return 0;
}
小结
一维差分的核心价值是将 O(n) 的区间操作降为 O(1),特别适合大规模数据批量更新场景。
三 . 二维前缀和
二维前缀和是一种用于快速计算矩阵子区域和的预处理技术,广泛应用于图像处理、动态规划等领域。
基本概念
二维前缀和是基于一维前缀和的扩展,它预先计算并存储从矩阵左上角(0,0)或(1,1)到任意点(i,j)形成的矩形区域内所有元素的和。
计算方法
前缀和数组构建
假设原始矩阵为mat[m][n],前缀和矩阵pre[m+1][n+1]的计算公式为:
pre[i][j] = mat[i][j] + pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1]
解释:
- mat[i][j]是当前元素
- pre[i-1][j]是上方矩形区域的和
- pre[i][j-1]是左侧矩形区域的和
- pre[i-1][j-1]是被重复计算的部分,需要减去
子矩阵和查询
给定子矩阵的左上角坐标(x1,y1)和右下角坐标(x2,y2),其和计算公式为:
sum = pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1]
解释:
pre[x2][y2]是整个大矩形区域的和
减去上方不需要的区域pre[x1-1][y2]
减去左侧不需要的区域pre[x2][y1-1]
加上被重复减去的左上角区域pre[x1-1][y1-1]
代码实现
#include <iostream>
using namespace std;
const int N = 1005;
int mat[N][N], pre[N][N];
int main() {
int m, n;
cin >> m >> n;
// 输入矩阵(1-based索引)
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
cin >> mat[i][j];
}
}
// 构建前缀和数组
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
pre[i][j] = mat[i][j] + pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1];
}
}
// 查询示例
int q, x1, y1, x2, y2;
cin >> q;
while(q--) {
cin >> x1 >> y1 >> x2 >> y2;
int sum = pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1];
cout << sum << endl;
}
return 0;
}
这段代码首先构建二维前缀和数组,然后可以快速查询任意子矩阵的和
应用场景
- 图像处理:快速计算图像局部区域像素值总和
- 动态规划:优化某些需要频繁计算子矩阵和的DP问题
- 游戏开发:计算地图区域内的资源总量
- 数据分析:快速统计二维数据表的区域汇总
时间复杂度分析
预处理:O(mn),只需遍历一次原始矩阵
查询:O(1),只需常数次运算即可得到结果
相比暴力计算的O(mn)查询时间,二维前缀和在多次查询场景下优势明显
注意事项
- 通常使用1-based索引更方便处理边界情况
- 前缀和数组大小应比原始矩阵多一行一列
- 注意整数溢出问题,特别是元素值较大时
差分是前缀和的逆运算,两者常结合使用。
四 . 二维差分
一、核心原理
1. 差分数组定义
设原矩阵为 A[i][j],其对应的二维差分矩阵 D[i][j] 满足:
A[i][j] = D[1][1] 到 D[i][j] 的前缀和
差分数组通过以下公式计算(边界外元素为0):
D[i][j] = A[i][j] - A[i-1][j] - A[i][j-1] + A[i-1][j-1]
此公式表示当前元素减去左方和上方元素,再加回被重复减去的左上角元素。
2. 子矩阵修改操作
对原矩阵的子矩阵 (x1,y1)(左上角)到 (x2,y2)(右下角)的所有元素 增加 c ,只需修改差分矩阵的四个点:
D[x1][y1]+=c // 影响右下所有区域
D[x1][y2+1]−=c // 抵消右侧多余区域
D[x2+1][y1]−=c // 抵消下方多余区域
D[x2+1][y2+1]+=c // 补偿重复抵消区域
原理:通过四个位置的增减,使修改仅作用于目标子矩阵(容斥原理)
二、实现步骤
1. 差分矩阵构建
- 方法一:直接按定义计算(推荐):
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
D[i][j] = A[i][j] - A[i-1][j] - A[i][j-1] + A[i-1][j-1];
- 方法二:插入法(初始化全0,逐步插入元素):
// 假设初始差分矩阵全0
void insert(int x1, int y1, int x2, int y2, int c) {
D[x1][y1] += c;
D[x1][y2+1] -= c;
D[x2+1][y1] -= c;
D[x2+1][y2+1] += c;
}
// 遍历原矩阵,插入每个元素
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
insert(i, j, i, j, A[i][j]); // 单点插入
2. 修改子矩阵
void add_submatrix(int x1, int y1, int x2, int y2, int c) {
D[x1][y1] += c;
D[x1][y2+1] -= c; // 超出右侧边界
D[x2+1][y1] -= c; // 超出下侧边界
D[x2+1][y2+1] += c; // 补偿重叠部分
}
3. 还原原矩阵
对差分矩阵求二维前缀和即可得到更新后的 A:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
A[i][j] = D[i][j] + A[i-1][j] + A[i][j-1] - A[i-1][j-1];
}
}
三、应用场景
- 动态区域更新
多次修改矩阵的子矩阵(如游戏地图资源刷新、图像滤镜处理)。
- 结合前缀和
先差分更新,再前缀和查询,实现高效“区间修改+区间查询”。
- 矩阵压缩
与一维差分嵌套使用,处理高维数据。
四、复杂度分析
- 构建差分矩阵:O(nm)
- 子矩阵修改:O(1)(仅修改4个点)
- 还原原矩阵:O(nm)(一次二维前缀和计算)
相比暴力更新(每次修改 O(nm)),在多次操作时优势显著
五、完整代码示例
#include <iostream>
using namespace std;
const int N = 1010;
int A[N][N], D[N][N]; // 原矩阵和差分矩阵
// 差分修改操作
void insert(int x1, int y1, int x2, int y2, int c) {
D[x1][y1] += c;
D[x1][y2+1] -= c;
D[x2+1][y1] -= c;
D[x2+1][y2+1] += c;
}
int main() {
int n, m, q;
cin >> n >> m >> q;
// 输入原矩阵 + 构建差分矩阵
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> A[i][j];
insert(i, j, i, j, A[i][j]); // 单点插入初始化
}
// 执行q次区域修改
while (q--) {
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c); // 修改子矩阵
}
// 通过前缀和还原更新后的矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
A[i][j] = D[i][j] + A[i-1][j] + A[i][j-1] - A[i-1][j-1];
cout << A[i][j] << " ";
}
cout << endl;
}
return 0;
}
六、注意事项
- 索引处理
推荐使用 1-based索引(数组从 [1][1] 开始),避免边界特判。
- 数组大小
差分矩阵需比原矩阵多一行一列(如 D[n+1][m+1]),防止越界。
- 溢出问题
大数值多次操作时需用 long long 防止溢出。
- 与一维差分关系
二维差分可视为一维差分的嵌套扩展,核心思想均为用单点操作表示区间修改。
总结
前缀和和差分是处理数据区间操作的核心技术,两者互逆,常结合使用以高效解决区间查询和区间修改问题。