前缀和是一种预处理技巧,核心思想 是:提前算好 "从起点到当前位置" 的累加和,用O (1) 时间快速求出任意区间和,把原本暴力 O (n) 的查询降到常数级。只能处理静态数组(不能边改边查,修改要 O (n))
# 一维数组前缀和
1)基本概念 ------ 假设有一个数组:
定义前缀和数组s :
,简单来说:
- s[i] 表示前 i 项的总和
- s[0]=0 ,是为了方便计算从第 1 项开始的区间,即数组的下标从1开始计算,也就是往前缀和数组中添加元素从1下标开始添加。但是当创建好一个数组,默认下标都是从0开始的,那么就单独处理一下s[0]的值,直接让它为0,由于创建好的数组已经被默认初始化为0,因此相当于无需处理。
- 原因:避免 i = 0 时出现 -1 下标越界,从1开始不用写额外判断判断0的情况,代码更干净
- 注意:如果想要让数组下标从1开始,那么数组大小应该是 n+1
2)核心思路:前缀和的作用就是快速求出数组中某一个连续区间的和。
- 想求原数组中 [l, r] 区间的和:

- 用前缀和公式可以直接算出:
- 该公式中,s[r] 表示前 r 项总和,**s[l−1]**表示前 l -1 项总和。
- 两者相减,就剩下从 l 到 r 的部分。
3)计算步骤:
1.预处理出一个前缀和数组
- 遍历一遍原数组arr,求出前缀和数组 s:s[i]=s[i−1]+arr[i] ------ 求和公式
- 解释公式:前缀和数组中的每一个元素都是原数组arr中前 i 个元素的和,即求出原数组中前 i 个元素的和(从下标1到 i),放到对应前缀和数组中的 i(从1开始) 位置
- 时间复杂度:O(n)
2.使用前缀和数组 查询
- 任意区间**[l,r]** 和 直接用公式sum[l,r] = s[r] - s[l-1]
- 即想要知道原数组前 i 个元素的和,直接在前缀和数组中利用上述查询。
- 时间复杂度:O(1)
3.示例:
有一个数组:a=[1,2,3,4,5],求区间 [2,4]的和。
以上题目的时间复杂度:
- 预处理前缀和数组:O(n)
单次区间和查询:O(1)
整体复杂度:O(n + q)(q 为查询次数)
对比暴力枚举:
- 暴力每次查询:O (n)
q 次查询就是 O(n·q),数据一大就会超时。
4)适用场景:
- 多次查询子数组和
求连续子数组和等于 k 的数量
二维数组的二维前缀和(求子矩阵和)
差分算法配套使用
接下来,我们通过一道算法题的练习,来熟悉这种算法思路。
1.【模板】一维前缀和
题目以及示例:
算法思路:这道题就是典型的前缀和算法,与前面的示例基本相同,直接写出代码即可。
java
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
//n表示原数组的大小,q表示查询任意区间的次数
System.out.print("输入数组大小:");
int n = in.nextInt();
System.out.print("输入查询次数:");
int q = in.nextInt();
System.out.print("输入数组arr中的元素:");
int[] arr = new int[n+1];
for (int i = 1; i <= n; i++) {
arr[i] = in.nextInt();
}
//1.预处理前缀和数组
long[] s = new long[n+1];//防止溢出
for (int i = 1; i <= n; i++) {
s[i] = s[i-1] + arr[i];
}
//2.查询
while (q > 0) {
System.out.print("输入要查询的区间的左区间:");
int l = in.nextInt();
System.out.print("输入要查询的区间的右区间:");
int r = in.nextInt();
System.out.print("该区间的和:");
System.out.println(s[r] - s[l-1]);
q--;
}
}
}
# 二维数组前缀和
在上一道题以及思路公式中,都是针对一维数组 的前缀和算法,当是二维数组 时,前缀和思路:
- 一维是求一段区间和,二维就是求一个子矩阵和。
- 思路完全是一维的扩展,只是公式稍微复杂一点。
- 依然是两个步骤:预处理一个前缀和数组 + 查询,即使用前缀和数组
1)基本概念
- 设原二维数组为a[i][j]
- 定义二维前缀和数组s[i][j] :s[i][j] = 从 左上角 (1,1) 到 右下角 (i,j) 的矩形内所有数的总和
- 下标从 (1,1) 开始而不是从 (0,0) 开始的原因与一维数组的一样,且s[0][0]=0
2)核心思路步骤:前缀和的作用就是快速求出数组中某一个子矩阵的和
- 1.求前缀和数组 s(预处理一个前缀和数组):
- 理解:s[i][j] 表示从 [1,1] 位置到 [i,j] 位置这段区间的所有元素之和,即前缀和数组中每个元素代表原数组 a 中的某个子矩阵的和。

- 为什么求前缀和数组中每个元素的值是上述的公式呢?如以下图解释:

- 预处理时间复杂度O(n·m)
- 理解:s[i][j] 表示从 [1,1] 位置到 [i,j] 位置这段区间的所有元素之和,即前缀和数组中每个元素代表原数组 a 中的某个子矩阵的和。
- 2.求任意子矩阵和(查询,使用前缀和数组):
- 理解:前面一个公式的目的是为了求出前缀和数组中每个元素的值,而这个公式是为了求出原数组中的某一个子矩阵的和,要使用到求好的前缀和公式。
- 公式分析如以下的图:

- 单次查询时间复杂度O(1),q次查询时间复杂度O(q)
- 3.二维前缀和整体时间复杂度O(nm+q)
3)一句话记忆:
- 构建/预处理前缀和:自己(D) + 上边(A+B) + 左边(A+C) - 左上角重复部分(A)
- 求子矩阵:大矩形(A+B+C+D) - 上面(A+B) - 左面(A+C) + 多减的左上角(A)
接下来,我们通过一道算法题的练习,来熟悉这种算法思路。
2.【模板】二维前缀和
题目以及示例:
算法思路:这道题就是典型的二维前缀和算法,直接根据公式写出代码即可。
java
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("矩阵的行和列:");
int n = in.nextInt(),m = in.nextInt();
System.out.println("查询次数:");
int q = in.nextInt();
//创建数组arr,并写入数据
int[][] arr = new int[n+1][m+1];
System.out.println("输入矩阵中的元素:");
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
arr[i][j] = in.nextInt();
}
}
//1.预处理前缀和数组s
long[][] s = new long[n+1][m+1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
s[i][j] = s[i-1][j]+s[i][j-1]+arr[i][j]-s[i-1][j-1];
}
}
//2.查询,使用前缀和数组
while (q > 0) {
//写入要查询的矩阵区间坐标
System.out.println("输入矩阵起始和结束坐标:");
int x1 = in.nextInt(),y1 = in.nextInt(),x2 = in.nextInt(),y2 = in.nextInt();
System.out.println(s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]);
q--;
}
}
}
3.寻找数组的中心下标
题目以及示例:
算法思路:根据题目意思,我们需要返回数组中某一个下标,且该下标的左侧元素之和等于右侧元素之和,如果是,那么就返回这个中心下标 i ,如果数组中有多个这样的中心下标,那么一定返回最左边的那个中心下标,如果数组中不存在这样的中心下标,则返回-1。
注意:此道题中,并不是模板中需要自己输入数组元素,而是已经给你一个数组nums,那么此时就不能再是可以自定从下标1开始放入元素,而是依据nums,是从0开始的,数组的大小就是n,但是在遍历时可以从下标1开始遍历,依然会提前处理下标为0时的情况。
这道题可以使用前缀和思路来做,可以预处理两个前缀和数组,即中心下标 i 前后两个区间,一个前缀个数组f 表示的是 [0,i-1] 这个区间所有元素的和,一个后缀和数组g 表示的是 [i+1,n-1] 这个区间所有元素的和。
处理细节问题:
- 根据题目,下标 0 左侧无元素,左侧和视为 0,下标 n-1 右侧无元素,右侧和视为 0;也就是说,前0个元素的和为0,后 n-1 个元素的和为0,那么:
- f[0] = 0
- g[n-1] = 0
- 前缀和数组f 是从左向右 开始添加元素的,而后缀和数组 g 是从右向左开始添加元素的,视 中心坐标 i 两边 i-1 和 i+1 为 f 和 g 的最后一个元素。
1)预处理前缀和后缀和数组:
- 前缀和数组 f:
- f[i] 表示原数组nums [0,i-1] 区间中的元素的前缀和
- 求 f 数组中元素的公式: f[i] = f[i-1] + nums[i-1]
- f[i] 表示原数组nums [0,i-1] 区间中的元素的前缀和
- 后缀和数组 g:
- g[i] 表示原数组 [i+1,n-1] 区间中元素的前缀和
- 求 g 数组中元素的公式: g[i] = g[i+1] + nums[i+1]
- g[i] 表示原数组 [i+1,n-1] 区间中元素的前缀和
2)使用前缀和后缀和数组:
- 遍历原数组nums,如果 i 的左右区间 f[i] == g[i] ,那么该下标为中心下标,直接返回改下标,这个下标一定是最左边的。

java
public int pivotIndex(int[] array) {
int n = array.length;
int[] f = new int[n];
int[] g = new int[n];
//f[0]与g[n-1]在创建数组时已经初始化为0了
//1.预处理
//前缀和
for (int i = 1; i < n; i++) {
f[i] = f[i-1] + array[i-1];
}
//后缀和
for (int i = n-2; i >= 0; i--) {
g[i] = g[i+1] + array[i+1];
}
//2.使用
for (int i = 0; i < n; i++) {
if (f[i] == g[i]) {
return i;
}
}
return -1;
}
4.除自身以外数组的乘积
题目以及示例:
算法思路:该题与上一道题类似,只不过这里求的是前缀积和后缀积,即遍历数组nums,求遍历到的除了 i 之外所有元素的乘积,并返回一个数组。
例如示例1中,返回的数组第一个元素24,就是nums中除了1之外所有元素的乘积,其他的一样。
处理细节:
- 与上一道题不同的是,这里的f[0] = 1,g[n-1] = 1,因为要算的是乘积,如果是0的话那么所有值都是0了。
- 依然 f 从左向右,g 从右向左。
1)预处理:
- f[i] = f[i-1] * nums[i-1]
- g[i] = g[i+1] * nums[i+1]
2)使用:
- 遍历nums,求出 i 的 f[i]*g[i] 的乘积,就是除了 i 外数组中所有元素的乘积,结果放在数组ret中,最后返回数组ret。
java
public int[] productExceptSelf(int[] arr) {
int n = arr.length;
int[] f = new int[n];
int[] g = new int[n];
//处理边界
f[0] = 1;
g[n-1] = 1;
//1.预处理
for (int i = 1; i < n; i++) {
f[i] = f[i-1] * arr[i-1];
}
for (int i = n-2; i >= 0; i--) {
g[i] = g[i+1] * arr[i+1];
}
//使用
int[] ret = new int[n];
for (int i = 0; i < n; i++) {
ret[i] = f[i] * g[i];
}
return ret;
}
5.和为k的子数组
题目以及示例:
算法思路:此道题要求的是nums中和为 k 的子数组的个数,且根据提示,nums中元素可以是0或者是负数,因此这道题是不可以使用 滑动窗口 来解决的,没有单调区间,且由于可以是0或者负数,left和right指针中间的某一段连续区间还可能也是目标区间,但是right永远无法往回走。
这道题的应该使用 前缀和 的思路来做,如果是暴力枚举的做法,是从头开始遍历数组寻找符合的子数组,即寻找以 i 位置开始的符合条件的子数组,那么根据暴力枚举的思路,利用前缀和思想就是 求 以 i 位置为结尾的所有子数组 ,即前 i 个元素的前缀和sum[i] 子数组,在这前 i 个元素的前缀和中,寻找和为 k 的子数组的个数,但是直接寻找这样的子数组比较难,我们可以通过寻找和为 sum[i] - k 的子数组的个数,间接知道了和为 k 的子数组个数。
即 求出以 i 位置为结尾的所有子数组,并在 [0,i-1] 区间内,求出前缀和等于 sum[i]-k 的个数。

注意 ,此时如果还是预处理一个前缀和数组的话,时间复杂度甚至会比暴力枚举还高(O(n^2)),原因:创建一个前缀和数组时间复杂度为 O(n),然后还需要在该前缀和数组中遍历找出 前缀和为sum[i]-k 的子数组 的个数,时间复杂度是O(n^2),即直接采用预处理前缀和数组+使用 这样的做法时间复杂度为 O(n^2+n)。
如果把前缀和存在哈希表中,而不是存在一个数组,那么时间复杂度就能优化到 O(n)。因此,这道题的做法是采用 前缀和+哈希表 的做法。
- 哈希表第一个参数表示 前缀和,第二个参数表示 次数。
总结:首先求出nums数组中前 i 个元素的前缀和,存放在哈希表中,然后在哈希表中寻找 和为sum[i]-k 的前缀和的个数,就是和为 k 的子数组的个数。
处理细节问题:
-
- 前缀和加入哈希表的时机?
- 在前面求前缀和数组的每个元素的值时的公式是 s[i] = s[i-1]+nums[i] ,即为前缀和数组中已经求好的前 i-1 个元素的和 + 该 i 位置上的元素,类比这个,那么应该是 计算 i 位置之前的前缀和加入哈希表,哈希表里面只保存 [0,i-1] 位置的前缀和。
-
- 不用真的创建一个前缀和数组,那么前一个位置的前缀和 s[i-1] 用一个变量 sum 来替换即可,即 sum = sum + nums[i]。
-
- 如果整个前缀和等于 k 呢?
- 意味着整个原数组元素的和才等于 k,那么此时前一个位置的前缀和变成了 下标为-1的位置开始,即数组之前 ,越界了,那么我们可以提前将这个前缀和定义为 0 ,即在哈希表中预先存入 前缀和 0,计数为 1: hash<0,1> 【=> s[0] = 0】。
-
- 使用变量count 计数,当有前缀和等于 sum-k 时,count++,表示和为 k 的子数组的个数
java
public int subarraySum(int[] nums,int k) {
Map<Integer,Integer> hash = new HashMap<>();
hash.put(0,1);
int sum = 0,count = 0;
for(int n : nums) {
sum += n;//计算当前位置的前缀和
count += hash.getOrDefault(sum-k,0);//统计当前位置的前一个位置前缀和中和为sum-k的前缀和个数
hash.put(sum,hash.getOrDefault(sum,0)+1);//把当前前缀和保存到哈希表中
}
return count;
}
6.和可被 k 整除的子数组
题目以及示例:
算法思路:此道题与上一道题的算法思路相同。
我们先了解一下同余定理:
求出前 i 个元素的前缀和sum,假设 子数组x 是和可被 k 整除的子数组,那么要统计 子数组x 的个数,依然可以通过求出 和为 sum-x 的前缀和可以被 k 整除 的个数 而 间接求出 x 的个数:
- 根据同余定理:由于 (sum-x)%k =0,那么 sum%k = x%k,进一步的,可以直接求可被k整除的 前缀和sum 的个数,来间接求出 x 的个数。
但是,还有一个问题 ------同余的数,取模结果符号不同 ,由于此道题中,nums中元素也可以是0或者负数,这就有可能出现问题,例如,-5%2 和 5%2 的结果其实是一样的,但是哈希表会把本该匹配的余数当成不同值,因此,需要对符号进行修正,将取余结果负数 修正成 正数 ,
修正公式为:a%p ------>变成a%p + p,例如,-5%2+2=1,负数就被修正成了正数。
当时,原本是正数的除数如果使用上述的公式,结果就不正确了,所以,就有
正负统一的修正公式: (a%p + p) % p,无论是正数还是负数,通过这个公式结果都是相同的。
接下来其他的细节等问题都和上一道题一样,不过该题中哈希表中存放的是前缀和的余数值。
java
public int subarraysDivByK(int[] nums,int k) {
Map<Integer,Integer> hash = new HashMap<>();
hash.put(0,1);//哈希表中存放余数,先处理是整个前缀和才被k整除的情况
//hash.put(0 % k,1);
int sum = 0,count = 0;
for(int n : nums) {
sum += n;//计算当前位置的前缀和
int r = (sum % k + K) % k;//计算当前位置的前缀和余数
count += hash.getOrDefault(r,0);//统计符合sum%k=0的前缀和个数
hash.put(r,hash.getOrDefault(r,0) + 1);//把当前的sum%k结果保存到哈希表中
}
return count;
}
7. 连续数组
题目以及示例:
算法思路:与前两道题类似,不过这里要找的是符合条件的最长子数组。
题目给了一个只有0和1的二进制数组nums,要找出0和1数量相同的一个最长子数组,我们可以转换思路:把 0 转化成 -1 ,即找出 -1 和 1数量相同的最长子数组,也就是和为0 的最长子数组 ,就相当于第5题的 和为k的子数组,因此,我们可以按照这个思路来做这道题 ------ 前缀和+哈希表:
- 在前缀和sum[i] 中,可以通过 sum[i]-0 间接找出 和为0的子数组 ,即找出前缀和sum[i] 中同样等于 sum 的那个****最短 的子数组 ,这样求出的那个 和为0的子数组 就是最长的子数组。
细节问题:
- 1.哈希表中存什么?
- 在 和为k的子数组 那道题中,求的是符合和为k的子数组的所有个数,即hash中存的分别是 前缀和 以及 这个前缀和出现的个数。
- 但是在此道题中,求的是 和为0的最长子数组,只需要计算出那一个最长的即可,即hash中存的是最短前缀和 以及 这个前缀和出现的最小的下标。
-
- 存入哈希表的时机?
- 与那一道题一样,都是使用完之后,再存入哈希表中。
-
- 哈希表中有重复的 hash<sum,i> ,即有重复的前缀和相同的元素,但是下标不同,如何存?
- 只需要保留前面已经存入的 <sum,i> 即可,因为已经存入的 sum,这个前缀和子数组的长度肯定比后存入的短,但是大小却一样,又由于要求出 和为0的最长子数组,因此只需要保留已经存入。
-
- 这个前缀和数组的和才等于0,该如何存?
- 在前一道题中,是提前设置好有一个前缀和为0的情况,即hash<0,1>
- 但是,此题中,要计算长度,且要记录的不是个数,而是下标,因此,要提前设置好有一个前缀和为0 的最小下标是-1的情况,即hash<0,-1>
-
- 长度如何算?
- 如下图,在和为sum的数组中,有一个也是和为sum的最短子数组,那么和为0的子数组的长度是 i-j+1,但是和为0的子数组并不包含 j 位置上的元素,因此长度计算:
- i-j+1-1 = i-j

java
public int findMaxLength(int[] nums) {
Map<Integer,Integer> hash = new HashMap<>();
hash.put(0,-1);//默认有一个前缀和为0的情况,前缀和为0的最小下标为-1
int sum = 0,ret = 0;
for(int i = 0;i < nums.length; i++) {
sum += (nums[i] == 0 ? -1 : 1);
if(hash.containsKey(sum)) {
ret = Math.max(ret,i - hash.get(sum));
}else {
hash.put(sum,i);
}
}
return ret;
}
8.矩阵区域和
题目以及示例:
算法思路:翻译一下题目的意思就是:以 (i,j) 为中心,向上下左右各扩展 k 格,形成一个 "矩形区域",求这个区域内所有元素的和(超出矩阵边界的部分忽略)。

使用二维前缀和的思路来解这道题。
那么:
- 求前缀和数组的公式:sum[i][j] = sum[i][j-1] + sum[i-1][j] - sum[i-1][j-1] + array[i][j]
- 求任意矩阵和的公式:sum = sum[x2][y2] - sum[x2][y1-1] - sum[x1-1][y2] + sum[x1-1][y1-1]
在此道题中,关于 answer[i,j],有一些细节需要处理:
关于下标映射关系(就是最开始讲二维前缀和思路时,创建的前缀和数组大小+1):
java
public static int[][] matrixBlockSum(int[][] mat,int k) {
int m = mat.length,n = mat[0].length;
//1.预处理前缀和数组
int[][] answer = new int[m+1][n+1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
answer[i][j] = answer[i][j-1]+answer[i-1][j]-answer[i-1][j-1]+mat[i-1][j-1];
}
}
//2.使用
int[][] ret = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int x1 = Math.max(0,i-k)+1,y1 = Math.max(0,j-k)+1;
int x2 = Math.min(m-1,i+k)+1,y2 = Math.min(n-1,j+k)+1;
ret[i][j] = answer[x2][y2]-answer[x1-1][y2]-answer[x2][y1-1]+answer[x1-1][y1-1];
}
}
return ret;
}