前缀和是一种预处理优化技术 ,核心思想是通过提前计算"前缀累计值",将数组/矩阵的区间查询操作 从暴力遍历的 O(n)O(n)O(n) 优化到 O(1)O(1)O(1),同时也能结合哈希表解决"子数组和"等复杂问题。
它的应用场景非常广泛:既可以处理基础的"多次区间和查询",也能扩展到二维矩阵的区域和,还能结合哈希表解决"子数组和为目标值""最长平衡子数组"等问题。本文将通过8道经典题目,拆解前缀和在不同场景下的解题逻辑与代码实现。
一、一维前缀和(模板题)
题目描述:
给定长度为 n 的数组和 m 次查询,每次查询给出区间 [l, r],输出该区间内元素的和。
示例:
- 输入:
3 2→ 数组[1,2,4]→ 查询[1,2]、[2,3] - 输出:
3、6
解题思路:
预处理"前缀和数组"dp,其中 dp[i] 表示前 i 个元素的累加和(数组下标从1开始,方便边界处理)。
- 前缀和公式:
dp[i] = dp[i-1] + arr[i] - 区间
[l, r]的和:dp[r] - dp[l-1](用前r个和减去前l-1个和,得到中间区间的和)
完整代码:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> arr(n + 1);
for(int i = 1; i <= n; i++) cin >> arr[i];
vector<long long> dp(n + 1);
for(int i = 1; i <= n; i++) dp[i] = dp[i - 1] + arr[i];
int l = 0, r = 0;
while(m--)
{
cin >> l >> r;
cout << dp[r] - dp[l - 1] << endl;
}
return 0;
}
复杂度分析:
- 时间复杂度:预处理 O(n)O(n)O(n),每次查询 O(1)O(1)O(1),总复杂度 O(n+m)O(n + m)O(n+m)。
- 空间复杂度:O(n)O(n)O(n),存储前缀和数组。
二、二维前缀和(模板题)
题目描述:
给定 n×m 的矩阵和 q 次查询,每次查询给出子矩阵的左上角 (x1,y1) 和右下角 (x2,y2),输出该子矩阵内元素的和。
示例:
- 输入:
3 4 3→ 矩阵[[1,2,3,4],[3,2,1,0],[1,5,7,8]]→ 查询(1,1,2,2)等 - 输出:
8、25、32
解题思路:
预处理"二维前缀和数组"dp,其中 dp[i][j] 表示以 (1,1) 为左上角、(i,j) 为右下角的子矩阵和。
- 二维前缀和公式:
dp[i][j] = arr[i][j] + dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1](加当前元素,加左、上区域和,减重复计算的左上区域和) - 子矩阵和公式:
dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1](减左、上区域,加回重复减去的左上区域)
完整代码:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, m, q;
cin >> n >> m >> q;
vector<vector<int>> arr(n + 1, vector<int>(m + 1));
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin >> arr[i][j];
vector<vector<long long>> dp(n + 1, vector<long long>(m + 1));
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
dp[i][j] = arr[i][j] + dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1];
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
while(q--)
{
cin >> x1 >> y1 >> x2 >> y2;
cout << dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1] << endl;
}
}
复杂度分析:
- 时间复杂度:预处理 O(n×m)O(n×m)O(n×m),每次查询 O(1)O(1)O(1),总复杂度 O(n×m+q)O(n×m + q)O(n×m+q)。
- 空间复杂度:O(n×m)O(n×m)O(n×m),存储二维前缀和数组。
三、寻找数组的中心下标
题目描述:
数组的中心下标 是指其左侧所有元素和等于右侧所有元素和的下标(若在两端,对应侧和视为0)。返回最靠左的中心下标,不存在则返回 -1。
示例:
- 输入:
nums = [1,7,3,6,5,6],输出:3(左侧和1+7+3=11,右侧和5+6=11)
解题思路:
预处理前缀和 (左到右累加)和后缀和(右到左累加):
- 前缀和数组
f:f[i]是nums[0..i-1]的和(i的左侧和)。 - 后缀和数组
g:g[i]是nums[i+1..n-1]的和(i的右侧和)。 - 遍历每个下标
i,判断f[i] == g[i],返回第一个满足条件的下标。
完整代码:
cpp
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n = nums.size();
vector<int> f(n), g(n);
for(int i = 1; i < n; i++)
f[i] = f[i - 1] + nums[i - 1];
for(int i = n - 2; i >= 0; i--)
g[i] = g[i + 1] + nums[i + 1];
for(int i = 0; i < n; i++)
if(f[i] == g[i])
return i;
return -1;
}
};
复杂度分析:
- 时间复杂度:O(n)O(n)O(n),预处理前缀和、后缀和及遍历各占 O(n)O(n)O(n)。
- 空间复杂度:O(n)O(n)O(n),存储前缀和、后缀和数组。
四、除自身以外数组的乘积
题目描述:
返回数组 answer,其中 answer[i] 是 nums 中除 nums[i] 外其余元素的乘积,要求不使用除法且时间复杂度为 O(n)O(n)O(n)。
示例:
- 输入:
nums = [1,2,3,4],输出:[24,12,8,6]
解题思路:
类比前缀和,预处理前缀积 和后缀积:
- 前缀积数组
f:f[i]是nums[0..i-1]的乘积(i左侧元素的乘积)。 - 后缀积数组
g:g[i]是nums[i+1..n-1]的乘积(i右侧元素的乘积)。 - 每个位置的结果:
answer[i] = f[i] * g[i]。
完整代码:
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> f(n), g(n);
f[0] = g[n - 1] = 1;
vector<int> ret(n);
for(int i = 1; i < n; i++)
f[i] = f[i - 1] * nums[i - 1];
for(int i = n - 2; i >= 0; i--)
g[i] = g[i + 1] * nums[i + 1];
for(int i = 0; i < n; i++)
ret[i] = f[i] * g[i];
return ret;
}
};
复杂度分析:
- 时间复杂度:O(n)O(n)O(n),预处理前缀积、后缀积及遍历各占 O(n)O(n)O(n)。
- 空间复杂度:O(n)O(n)O(n),存储前缀积、后缀积数组(可优化为 O(1)O(1)O(1),用结果数组临时存储)。
五、和为 K 的子数组
题目描述:
统计数组中和为 k 的连续子数组的个数。
示例:
- 输入:
nums = [1,1,1], k = 2,输出:2(子数组[1,1]出现两次)
解题思路:
用前缀和 + 哈希表优化:
- 定义前缀和
sum:遍历到当前元素的累加和。 - 哈希表
hash存储"前缀和出现的次数"(初始存入{0:1},对应前缀和为0的情况)。 - 遍历数组时,若
sum - k存在于哈希表中,说明存在以当前元素结尾、和为k的子数组,累加其出现次数;最后将当前sum加入哈希表。
完整代码:
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> hash;
hash[0] = 1;
int sum = 0, ret = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i];
if(hash.count(sum - k)) ret += hash[sum - k];
hash[sum]++;
}
return ret;
}
};
复杂度分析:
- 时间复杂度:O(n)O(n)O(n),遍历数组一次,哈希表操作均为 O(1)O(1)O(1)。
- 空间复杂度:O(n)O(n)O(n),哈希表最多存储 nnn 个不同的前缀和。
六、和可被 K 整除的子数组
题目描述:
统计数组中和可被 k 整除的连续子数组的个数。
示例:
- 输入:
nums = [4,5,0,-2,-3,1], k = 5,输出:7
解题思路:
利用同余性质 :若两个前缀和 sum1、sum2 对 k 取模的结果相同,则 sum2 - sum1 可被 k 整除。
- 前缀和
sum遍历累加,计算其对k的模r(处理负数模:(sum % k + k) % k)。 - 哈希表
hash存储"模值出现的次数"(初始存入{0:1})。 - 遍历数组时,若
r存在于哈希表中,累加其出现次数;最后将当前r加入哈希表。
完整代码:
cpp
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int, int> hash;
hash[0] = 1;
int sum = 0, ret = 0;
for(auto x : nums)
{
sum += x;
int r = (sum % k + k) % k;
if(hash.count(r)) ret += hash[r];
hash[r]++;
}
return ret;
}
};
复杂度分析:
- 时间复杂度:O(n)O(n)O(n),遍历数组一次,哈希表操作均为 O(1)O(1)O(1)。
- 空间复杂度:O(k)O(k)O(k),模值范围为
0~k-1,哈希表最多存储k个键。
七、连续数组
题目描述:
给定二进制数组,找到包含相同数量 0 和 1 的最长连续子数组,返回其长度。
示例:
- 输入:
nums = [0,1,0],输出:2(子数组[0,1]或[1,0])
解题思路:
将 0 转化为 -1,问题转化为"寻找和为 0 的最长连续子数组",再用前缀和 + 哈希表:
- 前缀和
sum遍历累加(0加-1,1加1)。 - 哈希表
hash存储"前缀和第一次出现的下标"(初始存入{0: -1},对应前缀和为0的初始状态)。 - 遍历数组时,若
sum已存在,计算当前下标与第一次出现下标的差,更新最长长度;若不存在,存入当前下标。
完整代码:
cpp
class Solution {
public:
int findMaxLength(vector<int>& nums) {
unordered_map<int, int> hash;
hash[0] = -1;
int sum = 0, ret = 0;
for(int i = 0; i < nums.size(); i++)
{
sum += nums[i] == 0 ? -1 : 1;
if(hash.count(sum)) ret = max(ret, i - hash[sum]);
else hash[sum] = i;
}
return ret;
}
};
复杂度分析:
- 时间复杂度:O(n)O(n)O(n),遍历数组一次,哈希表操作均为 O(1)O(1)O(1)。
- 空间复杂度:O(n)O(n)O(n),哈希表最多存储 nnn 个不同的前缀和。
八、矩阵区域和
题目描述:
给定 m×n 矩阵 mat 和整数 k,返回矩阵 answer,其中 answer[i][j] 是 mat 中以 (i,j) 为中心、边长为 2k+1 的正方形区域内的元素和(区域需在矩阵内)。
示例:
- 输入:
mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1,输出:[[12,21,16],[27,45,33],[24,39,28]]
解题思路:
结合二维前缀和:
- 预处理二维前缀和数组
dp(同"二维前缀和模板")。 - 遍历每个位置
(i,j),确定查询区域的边界:- 左上角
(x1, y1):max(0, i-k) + 1(加1是因为前缀和数组下标从1开始) - 右下角
(x2, y2):min(m-1, i+k) + 1
- 左上角
- 用二维前缀和公式计算该区域的和,存入
answer[i][j]。
完整代码:
cpp
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
int m = mat.size(), n = mat[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + mat[i - 1][j - 1];
}
vector<vector<int>> ret(m, vector<int>(n));
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
{
int x1 = max(0, i - k) + 1, y1 = max(0, j - k) + 1;
int x2 = min(m - 1, i + k) + 1, y2 = min(n - 1, j + k) + 1;
ret[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1];
}
return ret;
}
};
复杂度分析:
- 时间复杂度:预处理 O(m×n)O(m×n)O(m×n),遍历计算结果 O(m×n)O(m×n)O(m×n),总复杂度 O(m×n)O(m×n)O(m×n)。
- 空间复杂度:O(m×n)O(m×n)O(m×n),存储二维前缀和数组。