前缀和算法:从一维到二维,解锁高效区间求和
在算法解题中,我们经常会遇到多次查询区间和、子数组和、子矩阵和 的问题,如果每次查询都暴力遍历计算,时间复杂度会居高不下(如O(n)或O(nm)每次查询),在数据量较大时极易超时。而前缀和算法 正是解决这类问题的最优解之一,它通过一次预处理将区间和查询的时间复杂度降至O(1),大幅提升程序效率。
本文将从一维前缀和 入手,逐步延伸到二维前缀和,结合经典例题讲解算法原理、实现步骤和实际应用,同时补充前缀和的拓展用法(如前缀积、哈希表结合前缀和),让你彻底掌握这一基础且实用的算法。
一、一维前缀和:基础中的基础
1. 算法原理
一维前缀和的核心思想是预处理出一个前缀和数组 ,其中每个位置的值表示原数组从起始位置到当前位置的所有元素之和。通过这个预处理的数组,我们可以快速推导出任意区间[l, r]的元素和。
设原数组为arr[1...n](为了方便处理边界,通常将数组下标从1开始),定义前缀和数组pre[1...n],其中:
pre[i]=arr[1]+arr[2]+...+arr[i]pre[i] = arr[1] + arr[2] + ... + arr[i]pre[i]=arr[1]+arr[2]+...+arr[i]
根据上述定义,可推出递推公式:
pre[i]=pre[i−1]+arr[i]pre[i] = pre[i-1] + arr[i]pre[i]=pre[i−1]+arr[i]
(初始条件:pre[0] = 0,表示空区间的和为0)
有了前缀和数组后,任意区间 [l, r] 的和可通过公式快速计算:
sum(l,r)=pre[r]−pre[l−1]sum(l, r) = pre[r] - pre[l-1]sum(l,r)=pre[r]−pre[l−1]
2. 模板实现(以Java为例)
适用于多次查询一维数组任意区间和 的场景,题目要求通常为:给定长度为n的数组,q次查询,每次查询给出l和r,输出arr[l...r]的和。
Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int q = sc.nextInt();
// 原数组和前缀和数组,下标从1开始,避免边界问题
long[] arr = new long[n + 1];
long[] pre = new long[n + 1];
// 读入原数组
for (int i = 1; i <= n; i++) {
arr[i] = sc.nextLong();
}
// 预处理前缀和数组
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1] + arr[i];
}
// 处理q次查询
while (q-- > 0) {
int l = sc.nextInt();
int r = sc.nextInt();
// 区间和公式
System.out.println(pre[r] - pre[l - 1]);
}
sc.close();
}
}
关键注意点:
-
数组下标从1开始,避免处理
l=1时pre[l-1]=pre[0]的边界问题; -
使用
long类型,防止数组元素值过大或数量过多时求和溢出。
3. 经典例题:寻找数组的中心下标
题目链接:LeetCode 724. 寻找数组的中心下标
题目描述:找到数组的一个下标,其左侧所有元素的和等于右侧所有元素的和,若有多个返回最左侧的,无则返回-1。
解题思路:
-
预处理前缀和数组
pre,pre[i]表示arr[0...i-1]的和(适配原数组0下标); -
对于下标
i,左侧和为pre[i],右侧和为pre[n] - pre[i+1]; -
遍历数组,找到第一个满足
pre[i] == pre[n] - pre[i+1]的下标即可。
核心代码(Java):
Java
public int pivotIndex(int[] nums) {
int n = nums.length;
int[] pre = new int[n+1];
// 预处理前缀和
for (int i = 1; i <= n; i++) pre[i] = pre[i-1] + nums[i-1];
// 遍历判断中心下标
for (int i = 0; i < n; i++) {
int left = pre[i];
int right = pre[n] - pre[i+1];
if (left == right) return i;
}
return -1;
}
二、二维前缀和:解决子矩阵和问题
当问题从一维数组 延伸到二维矩阵 ,需要多次查询子矩阵的和时,一维前缀和就无法满足需求了,此时需要二维前缀和 ,其原理是一维前缀和的拓展,核心仍是预处理前缀和矩阵。
1. 算法原理
设原矩阵为mat[1...n][1...m](下标从1开始),定义二维前缀和矩阵pre[1...n][1...m],其中pre[i][j]表示原矩阵中从左上角(1,1)到右下角(i,j) 的子矩阵的所有元素之和。
(1)前缀和矩阵的递推公式
要计算pre[i][j],可将其拆分为四个部分(画图理解更直观):
-
上方子矩阵:
pre[i-1][j] -
左方子矩阵:
pre[i][j-1] -
重复计算的左上角子矩阵:
pre[i-1][j-1](需要减去) -
当前位置元素:
mat[i][j]
递推公式:
pre[i][j]=pre[i−1][j]+pre[i][j−1]−pre[i−1][j−1]+mat[i][j]pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + mat[i][j]pre[i][j]=pre[i−1][j]+pre[i][j−1]−pre[i−1][j−1]+mat[i][j]
初始条件:pre[0][*] = pre[*][0] = 0,即第一行和第一列全为0,处理边界更方便。
(2)任意子矩阵的和计算
对于原矩阵中左上角(x1,y1)到右下角(x2,y2) 的子矩阵,其和的计算公式为:
sum(x1,y1,x2,y2)=pre[x2][y2]−pre[x1−1][y2]−pre[x2][y1−1]+pre[x1−1][y1−1]sum(x1,y1,x2,y2) = pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1]sum(x1,y1,x2,y2)=pre[x2][y2]−pre[x1−1][y2]−pre[x2][y1−1]+pre[x1−1][y1−1]
公式理解:用整个大矩阵和pre[x2][y2],减去上方多余部分、左方多余部分,再加上重复减去的左上角部分。
2. 模板实现(以Java为例)
适用于多次查询二维矩阵任意子矩阵和的场景,题目要求通常为:给定n行m列矩阵,q次查询,每次查询给出x1,y1,x2,y2,输出对应子矩阵的和。
Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 行数
int m = sc.nextInt(); // 列数
int q = sc.nextInt(); // 查询次数
// 原矩阵和前缀和矩阵,下标从1开始
int[][] mat = new int[n + 1][m + 1];
long[][] pre = new long[n + 1][m + 1];
// 读入原矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
mat[i][j] = sc.nextInt();
}
}
// 预处理二维前缀和矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + mat[i][j];
}
}
// 处理q次查询
while (q-- > 0) {
int x1 = sc.nextInt();
int y1 = sc.nextInt();
int x2 = sc.nextInt();
int y2 = sc.nextInt();
// 子矩阵和公式
long sum = pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1];
System.out.println(sum);
}
sc.close();
}
}
3. 经典例题:矩阵区域和
题目链接:LeetCode 1314. 矩阵区域和
题目描述 :给定m x n矩阵mat和整数k,返回矩阵answer,其中answer[i][j]是mat中以(i,j)为中心,上下左右k个范围内的所有元素和(超出矩阵边界的部分忽略)。
解题思路:
-
预处理二维前缀和矩阵
pre; -
对于每个位置(i,j),计算其有效区域的左上角(x1,y1) 和右下角(x2,y2):
-
x1 = max(0, i-k) + 1,y1 = max(0, j-k) + 1(适配原矩阵0下标,转换为前缀和矩阵1下标);
-
x2 = min(m-1, i+k) + 1,y2 = min(n-1, j+k) + 1;
-
代入二维前缀和公式计算该区域和,赋值给
answer[i][j]。
核心代码(Java):
Java
public int[][] matrixBlockSum(int[][] mat, int k) {
int m = mat.length, n = mat[0].length;
int[][] pre = new int[m+1][n+1];
// 预处理二维前缀和
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + mat[i-1][j-1];
// 计算结果矩阵
int[][] ans = 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;
int y1 = Math.max(0, j - k) + 1;
int x2 = Math.min(m-1, i + k) + 1;
int y2 = Math.min(n-1, j + k) + 1;
// 二维前缀和公式
ans[i][j] = pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1];
}
}
return ans;
}
三、前缀和的拓展用法:不止于求和
前缀和的核心思想是预处理前序累积信息 ,这一思想可拓展到前缀积、哈希表结合前缀和等场景,解决更多经典问题,如子数组和为k、除自身以外数组的乘积等。
1. 前缀积:除自身以外数组的乘积
题目链接:LeetCode 238. 除自身以外数组的乘积
题目描述:返回一个数组,其中每个元素是原数组中除当前元素外所有元素的乘积,要求不能使用除法,时间复杂度O(n)。
解题思路:
-
预处理前缀积数组
left:left[i]表示原数组[0...i-1]的乘积; -
预处理后缀积数组
right:right[i]表示原数组[i+1...n-1]的乘积; -
结果数组
ans[i] = left[i] * right[i]。
核心代码(Java):
Java
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] left = new int[n];
int[] right = new int[n];
left[0] = 1;
right[n-1] = 1;
// 预处理前缀积
for (int i = 1; i < n; i++) left[i] = left[i-1] * nums[i-1];
// 预处理后缀积
for (int i = n-2; i >= 0; i--) right[i] = right[i+1] * nums[i+1];
// 计算结果
int[] ans = new int[n];
for (int i = 0; i < n; i++) ans[i] = left[i] * right[i];
return ans;
}
2. 哈希表 + 前缀和:子数组和为k
题目链接:LeetCode 560. 和为 K 的子数组
题目描述:统计数组中和为k的连续子数组的个数,时间复杂度要求优于O(n²)。
解题思路:
-
设前缀和为
sum,表示从数组起始到当前位置的和; -
对于当前位置
i,若存在位置j < i使得sum[i] - sum[j] = k,则子数组[j+1...i]的和为k; -
用哈希表 记录每个前缀和出现的次数,遍历数组时,查询
sum - k出现的次数,即为当前位置能形成的和为k的子数组个数。
核心代码(Java):
Java
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // 初始前缀和0出现1次,处理sum=k的情况
int sum = 0, count = 0;
for (int num : nums) {
sum += num;
// 查询sum-k出现的次数,累加到结果
count += map.getOrDefault(sum - k, 0);
// 更新哈希表,当前前缀和出现次数+1
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
return count;
}
同类拓展题:
-
LeetCode 974. 和可被 K 整除的子数组(结合同余定理,哈希表记录前缀和取模的次数);
-
LeetCode 525. 连续数组(将0转为-1,问题转化为找和为0的最长子数组,哈希表记录前缀和第一次出现的位置)。
四、前缀和算法的核心总结
1. 适用场景
-
一维/二维数组多次查询区间和/子矩阵和;
-
寻找满足特定和条件的连续子数组(如和为k、和可被k整除、0和1数量相等);
-
需计算前序/后序累积信息(如前缀积、后缀积)的问题。
2. 核心优势
-
预处理O(n) / O(nm):仅需一次遍历即可完成前缀和数组/矩阵的预处理;
-
查询O(1):后续每次查询只需代入公式,无需重复遍历;
-
实现简单:无复杂逻辑,仅需掌握递推公式和查询公式,边界处理(下标从1开始)是关键。
3. 关键注意点
-
数据溢出 :优先使用
long long(C++)或long(Java)类型存储前缀和/积; -
下标处理 :建议将原数组/矩阵的下标从1开始,避免处理
l=0或x1=0的边界问题; -
哈希表初始化 :结合哈希表的前缀和问题,需初始化
map.put(0, 1),处理前缀和直接等于目标值的情况; -
负数处理 :涉及取模的前缀和问题(如和可被k整除),需修正负数取模的结果,公式为
(sum % k + k) % k,保证余数为正。
五、写在最后
前缀和算法是算法入门阶段的基础必学算法 ,它不仅是解决区间和问题的最优解,更体现了空间换时间的经典算法思想------通过消耗少量的存储空间预处理信息,换取查询时的时间效率提升。
掌握一维和二维前缀和的基本原理后,再结合哈希表、同余定理等知识,就能解决大部分与连续子数组/子矩阵和相关的问题。建议大家结合本文的例题动手实现代码,理解公式的推导过程,而非死记硬背,这样才能灵活运用到各种变式问题中。
后续在学习差分算法时,你会发现差分是前缀和的逆运算,二者结合能解决更多数组修改与查询的综合问题,不妨提前做好铺垫~