前缀和完全指南
-
算法
-
前缀和
-
差分
-
算法入门
-
数据结构 categories:
-
算法入门
前缀和是算法入门最基础的技巧之一,它可以将区间求和的时间复杂度从 O (n) 优化到 O (1),是解决大量数组、矩阵问题的核心工具。本文从一维到二维,从基础到进阶,带你彻底搞懂前缀和的所有用法,包含完整的 C++ 代码实现和经典例题,完全适配算法竞赛风格,看完就能上手。
一、什么是前缀和?
前缀和(Prefix Sum),顾名思义,就是数组的前 i 个元素的累加和。
它的核心作用是:预处理之后,O (1) 时间查询任意区间的和。
如果没有前缀和,你每次查询区间[l,r]的和,都要遍历从 l 到 r 的所有元素,时间复杂度 O (n),如果有 q 次查询,总时间就是 O (nq),数据量大的时候会非常慢。
而有了前缀和,我们只需要 O (n) 的时间预处理,之后每次查询都是 O (1),总时间 O (n+q),效率提升了好几个量级。
一维前缀和的核心公式
对于原数组 a[1..n],我们定义前缀和数组 s[0..n],其中: s[i] = a[1] + a[2] + ... + a[i] 也就是前 i 个元素的累加和,其中 s[0] = 0,用来处理边界情况。
那么,任意区间 [l, r] 的和,就可以表示为:
sum(l, r) = s[r] - s[l-1]
这个公式是前缀和的核心,非常好理解:前 r 个元素的和,减去前 l-1 个元素的和,剩下的就是 l 到 r 的和。
举个例子
比如我们有原数组:
|--------|---|---|---|---|---|
| 下标 i | 1 | 2 | 3 | 4 | 5 |
| a[i] | 1 | 2 | 3 | 4 | 5 |
那么我们的前缀和数组 s 就是:
|--------|---|---|---|---|----|----|
| 下标 i | 0 | 1 | 2 | 3 | 4 | 5 |
| s[i] | 0 | 1 | 3 | 6 | 10 | 15 |
现在我们要查询区间 [2,4] 的和,也就是 2+3+4=9,用公式计算: s[4] - s[1] = 10 - 1 = 9,完全正确!
二、一维前缀和的实现
一维前缀和的实现非常简单,只需要遍历一遍数组,累加计算前缀和即可,下面是符合算法竞赛风格的完整代码:
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
ll n,a[maxn],s[maxn];
void sol(){
cin>>n;
for(ll i=1;i<=n;i++)cin>>a[i];//预处理前缀和
for(ll i=1;i<=n;i++)s[i]=s[i-1]+a[i];
ll q;
cin>>q;
while(q--){
ll l,r;
cin>>l>>r;
cout<<s[r]-s[l-1]<<endl;
}
}
int main(){
sol();
return 0;
}
注意:这里我们用了1-based 下标,也就是数组从 1 开始,这样可以避免 l=1 的时候 l-1=-1 的越界问题,s [0] 默认是 0,刚好处理边界,这是前缀和的标准写法,强烈推荐。
三、一维前缀和的经典应用
除了最基础的区间求和,一维前缀和还有很多常用的应用场景:
-
子数组和问题:比如求有多少个子数组的和等于 K,这就是 LeetCode 560 题,用前缀和 + 哈希表可以 O (n) 解决。
-
前缀和优化 DP:很多 DP 的状态转移里,有区间求和的操作,用前缀和可以把转移的时间从 O (n) 优化到 O (1)。
-
差分的逆运算:差分最后要通过前缀和得到最终的数组,这个我们后面会详细讲。
四、二维前缀和:矩阵的区间求和
一维的前缀和解决了数组的区间求和,那如果是矩阵,我们要查询一个子矩阵的和,怎么办?这时候就需要二维前缀和。
二维前缀和的核心公式
对于原矩阵 a[1..n][1..m],我们定义前缀和矩阵 s[0..n][0..m],其中 s[i][j] 表示从左上角 (1,1) 到右下角 (i,j) 的所有元素的和。
预处理公式:
s[i][j] = a[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1]
这个公式用到了容斥原理,我们来解释一下:
-
s[i-1][j]:当前位置上面的矩形的和 -
s[i][j-1]:当前位置左边的矩形的和 -
这两个加起来,
s[i-1][j-1]被加了两次,所以要减去一次 -
最后加上当前的 a [i][j],就是当前的 s [i][j]
查询公式
如果我们要查询左上角 (x1,y1),右下角 (x2,y2) 的子矩阵的和,公式是:
sum = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]
同样是容斥原理:
-
整个大矩形的和 s [x2][y2]
-
减去上面的部分 s [x1-1][y2]
-
减去左边的部分 s [x2][y1-1]
-
这时候,左上角的 s [x1-1][y1-1] 被减了两次,所以要加回来一次
举个例子
比如我们有 3x3 的原矩阵:
|---|---|---|---|
| | 1 | 2 | 3 |
| 1 | 1 | 2 | 3 |
| 2 | 4 | 5 | 6 |
| 3 | 7 | 8 | 9 |
预处理后的前缀和矩阵 s:
|---|---|----|----|----|
| | 0 | 1 | 2 | 3 |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 3 | 6 |
| 2 | 0 | 5 | 12 | 21 |
| 3 | 0 | 12 | 27 | 45 |
现在我们要查询子矩阵 (2,2) 到 (3,3) 的和,也就是 5+6+8+9=28,用公式计算: s[3][3] - s[1][3] - s[3][1] + s[1][1] = 45 -6 -12 +1 =28,完全正确!
五、二维前缀和的实现
二维前缀和的实现也很简单,按行遍历预处理即可,完整代码如下:
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1010;
ll n,m,q,a[maxn][maxn],s[maxn][maxn];
void sol(){
cin>>n>>m>>q;
for(ll i=1;i<=n;i++){
for(ll j=1;j<=m;j++){
cin>>a[i][j];
}
}
for(ll i=1;i<=n;i++){
for(ll j=1;j<=m;j++){
s[i][j]=a[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1];
}
}
while(q--){
ll x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<<endl;
}
}
int main(){
sol();
return 0;
}
六、进阶:差分,前缀和的逆运算
差分(Difference Array)是前缀和的逆运算,它可以把区间更新的操作,从 O (n) 优化到 O (1),最后只需要求一次前缀和,就能得到最终的数组,是区间更新的神器。
一维差分
定义差分数组 d,其中 d [i] = a [i] - a [i-1],也就是原数组的差分。
如果我们要对区间 [l,r] 的所有元素都加 v,那么只需要两步操作:
d[l] += v; d[r+1] -= v;
然后,最后对 d 求前缀和,就得到了更新后的 a 数组!
举个例子: 原数组 a 是[1,2,3,4,5],差分数组 d 是[0,1,1,1,1,1]。 现在我们要对区间 [2,4] 加 2,也就是 a [2],a [3],a [4] 都加 2,那么我们只需要: d[2] +=2,d[5] -=2,d 变成[0,1,3,1,1,-1]。 然后对 d 求前缀和,得到 a 变成[1,4,5,6,5],刚好是 2,3,4 都加了 2,完全正确!
二维差分
同理,二维的差分,就是对一个子矩阵的所有元素加 v,只需要 O (1) 操作,然后求前缀和得到结果。
公式是:
d[x1][y1] += v; d[x1][y2+1] -= v; d[x2+1][y1] -= v; d[x2+1][y2+1] += v;
然后最后对 d 求二维前缀和,就得到了更新后的矩阵。
七、经典例题
下面是几个前缀和的经典例题,你可以用来练习:
-
LeetCode 560. 和为 K 的子数组 给你一个整数数组 nums 和一个整数 k,统计数组中和为 k 的连续子数组的个数。 解法:用前缀和 + 哈希表,遍历的时候记录前缀和出现的次数,O (n) 解决。
-
LeetCode 304. 二维区域和检索 - 矩阵不可变 给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。 解法:就是二维前缀和的标准应用,预处理之后 O (1) 查询。
-
LeetCode 1109. 航班预订统计 这里有 n 个航班,它们分别从 1 到 n 进行编号。有一份预订列表 bookings,其中第 i 条记录 bookings [i] = [firsti, lasti, seatsi],意味着在 firsti 到 lasti 之间的每个航班都预订了 seatsi 个座位。请你返回一个长度为 n 的数组 answer,其中 answer [i] 是第 i 个航班的座位总数。 解法:一维差分的标准应用,区间更新,最后求前缀和。
八、前缀和的注意事项
用前缀和的时候,有几个常见的坑要注意:
-
下标从 1 开始:强烈推荐用 1-based 下标,避免 l=1 的时候 l-1=-1 的越界问题,s [0] 默认是 0,刚好处理边界,这是最常用的写法。
-
数据溢出:前缀和是累加的,很容易超过 int 的范围,所以一定要用 long long,不然会溢出,导致答案错误,这是很多新手常犯的错。
-
预处理顺序:二维前缀和的预处理,要按从小到大的顺序遍历,保证计算 s [i][j] 的时候,s [i-1][j]、s [i][j-1]、s [i-1][j-1] 都已经算好了。
-
容斥的符号:二维前缀和的公式里,最后那个加和减不要搞反,很多人会把加写成减,导致答案错误,一定要记清楚:减两个,加一个。
九、总结
前缀和是算法里最基础、最常用的技巧之一,它的思想非常简单,但是应用非常广泛,不管是入门的算法题,还是高级的算法,比如树状数组、线段树,其实都是前缀和的扩展。
学会了前缀和,你就已经解决了一大半的区间求和、区间更新的问题,这是算法入门的必经之路。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,我会持续更新算法入门的系列文章~