跳到主要内容
前缀和算法详解:一维、二维及哈希优化应用 | 极客日志
C++ 算法
前缀和算法详解:一维、二维及哈希优化应用 综述由AI生成 系统讲解前缀和算法,涵盖一维与二维前缀和的基础构建与应用。通过寻找数组中心下标、除自身以外数组的乘积等经典例题,展示了如何利用前缀和优化时间复杂度。进一步结合哈希表解决子数组求和问题(如和为 K、可被 K 整除、连续数组),并延伸至矩阵区域和场景。内容包含解题思路、代码实现及复杂度分析,旨在帮助读者掌握前缀和的核心思想与变体技巧。
二进制 发布于 2026/3/26 更新于 2026/5/11 19 浏览1. 一维前缀和
题目描述:
给定一个长度为 n 的整数数组 arr 和 q 个查询,每个查询由两个整数 l 和 r 组成,表示区间 [l, r]。请计算出每个区间内所有元素的和。
示例 1:
输入:arr = [1, 2, 3, 4, 5], q = 2, 查询区间为 [(1, 3), (2, 4)]
输出:[6, 9]
解释:区间 [1, 3] 的元素和为 1 + 2 + 3 = 6,区间 [2, 4] 的元素和为 2 + 3 + 4 = 9。
提示:
1 <= n, q <= 100000
-10000 <= arr[i] <= 10000
解题思路
对于本道题,我们可以提前预处理出一个前缀和数组 dp,通过前缀和数组快速求出某个区间所有元素的和。
前缀和数组 dp[i] 表示数组起始位置到第 i 个位置的所有元素和。其递推公式我们很容易得出:
dp[i] = dp[i - 1] + arr[i]
计算区间 [l, r] 的所有元素的和:
代码实现
#include <iostream>
#include <vector>
using namespace std;
int main () {
int n, q;
cin >> n >> q;
vector<int > arr (n + 1 ) ;
for (int i = 1 ; i <= n; ++i) cin >> arr[i];
vector<long long > ;
( i = ; i <= n; ++i) dp[i] = dp[i - ] + arr[i];
l, r;
(q--) {
cin >> l >> r;
cout << dp[r] - dp[l - ] << endl;
}
;
}
dp
(n + 1 )
for
int
1
1
int
while
1
return
0
细节处理 :我们开辟前缀和数组时开的空间大小应该比数组的大 1(开辟 n + 1 的空间),可以让数组的元素位置与下标一一对应(第一个元素对应下标 1 位置),帮助我们规避掉像 dp[1] 等边界情况。
2. 二维前缀和 给定一个大小为 n × m 的矩阵 matrix 和 q 个查询,每个查询由四个整数 x1, y1, x2, y2 组成,表示一个子矩阵的左上角 (x1, y1) 和右下角 (x2, y2)。请计算出每个子矩阵内所有元素的和。
输入:matrix = [[1, 2], [3, 4]], q = 1, 查询区间为 [(1, 1, 2, 2)]
输出:[10]
解释:子矩阵包含所有元素 1 + 2 + 3 + 4 = 10。
1 <= n, m <= 1000
-10000 <= matrix[i][j] <= 10000
这道题与上一道题类似,我们可以提前预处理出一个前缀矩阵和数组 dp,dp[i][j] 表示矩阵从 [1, 1] 位置到 [i, j] 位置的所有元素和,然后利用 dp 数组快速求出子矩阵内所有元素的和。
构建前缀和矩阵数组
递推公式:dp[i][j] = matrix[i][j] + dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1]
sum = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1]
计算 [x1, y1] -> [x2, y2] 任意子矩阵的和 sum
构建数组时与上题类似,每一行每一列多开一个空间以此来避免边界情况。
#include <iostream>
#include <vector>
using namespace std;
int main () {
int n = 0 , m = 0 , q = 0 ;
cin >> n >> m >> q;
vector<vector<int >> vv (n + 1 , vector <int >(m + 1 ));
for (int i = 1 ; i <= n; ++i) {
for (int j = 1 ; j <= m; ++j) cin >> vv[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] = dp[i - 1 ][j] + dp[i][j - 1 ] + vv[i][j] - dp[i - 1 ][j - 1 ];
}
int x1, y1, x2, y2;
while (q--) {
cin >> x1 >> y1 >> x2 >> y2;
long long ret = dp[x2][y2] - dp[x2][y1 - 1 ] - dp[x1 - 1 ][y2] + dp[x1 - 1 ][y1 - 1 ];
cout << ret << endl;
}
return 0 ;
}
3. 寻找数组的中心下标 给你一个整数数组 nums,请计算数组的 中心下标。
中心下标是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0,因为在下标的左侧不存在元素。这一点对中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1。
输入:nums = [1, 7, 3, 6, 5, 6]
输出:3
解释:
中心下标是 3。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11,二者相等。
输入:nums = [1, 2, 3]
输出:-1
解释:
输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0。
左侧数之和 sum = 0,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0。
1 <= nums.length <= 10^4
-1000 <= nums[i] <= 1000
解题思路
根据题目要求,我们可以预处理出一个前缀和数组 f 和后缀和数组 g,然后遍历数组通过比较前缀和和后缀和找到中心下标。
前缀和数组
f[i] 表示数组开始位置到 i - 1 位置的所有元素和
递推公式:f[i] = f[i - 1] + nums[i - 1]
后缀和数组
g[i] 表示数组 i + 1 位置到数组末尾位置的所有元素和
递推公式:g[i] = g[i + 1] + nums[i + 1]
class Solution {
public int pivotIndex (int [] nums) {
int n = nums.length;
int [] f = new int [n];
for (int i = 1 ; i < n; ++i) f[i] = f[i - 1 ] + nums[i - 1 ];
int [] g = new int [n];
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 ;
}
}
上面的解法其实还可以优化,设 nums 的所有元素之和为 S,中心下标为 i,i 左边的元素和为 lsum,根据左侧元素和与右侧元素和相等有:
lsum = S - nums[i] - lsum
所以我们只需用一个量 leftSum 记录当前元素的左侧元素和,根据推导公式判断当前位置是否为中心下标。
优化后的解法时间复杂度从 O(n) 降到了 O(1) 了(额外空间)。
class Solution {
public int pivotIndex (int [] nums) {
int sum = 0 ;
for (int e : nums) sum += e;
int leftSum = 0 ;
for (int i = 0 ; i < nums.length; ++i) {
if (2 * leftSum == sum - nums[i]) return i;
leftSum += nums[i];
}
return -1 ;
}
}
4. 除自身以外数组的乘积 给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
题目数据保证数组 nums 中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。
请不要使用除法 ,且在 O(n) 时间复杂度内完成此题。
输入:nums = [1, 2, 3, 4]
输出:[24, 12, 8, 6]
输入:nums = [-1, 1, 0, -3, 3]
输出:[0, 0, 9, 0, 0]
2 <= nums.length <= 10^5
-30 <= nums[i] <= 30
保证数组 nums 中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。
进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?(出于对空间复杂度分析的目的,输出数组不被视为额外空间。)
与上一题类似,我们先预处理出前缀积数组 f 和后缀积数组 g,然后将两者相乘得到除自身元素以外的乘积。
前缀积数组
f[i] 表示数组起始位置到第 i - 1 位置所有元素的积。
递推公式:f[i] = f[i - 1] * nums[i - 1]
后缀积数组
g[i] 表示数组第 i + 1 位置到数组末尾位置所有元素的积。
递推公式:g[i] = g[i + 1] * nums[i + 1]
细节:f[0] = g[n - 1] = 1,对于这两个数组初始情况需要为 1
构建出前缀积与后缀积数组后,我们直接遍历,两者相乘,构建出需要返回的数组 ret
class Solution {
public int [] productExceptSelf(int [] nums) {
int n = nums.length;
int [] f = new int [n];
for (int i = 1 ; i < n; ++i) f[i] = f[i - 1 ] * nums[i - 1 ];
int [] g = new int [n];
for (int i = n - 2 ; i >= 0 ; --i) g[i] = g[i + 1 ] * nums[i + 1 ];
int [] ret = new int [n];
for (int i = 0 ; i < n; ++i) ret[i] = f[i] * g[i];
return ret;
}
}
我们可以先处理出后缀积 g,用 pre 记录前缀积(初始化为 1),然后遍历将 pre 乘到 g[i] 中,同时维护 pre,最后将 g 返回即可。
这种写法相较于上种可以少遍历一次数组,而且题目说到输出数组不被视为额外空间,故时间复杂度为 O(1)(额外空间)。
class Solution {
public int [] productExceptSelf(int [] nums) {
int n = nums.length;
int [] g = new int [n];
for (int i = n - 2 ; i >= 0 ; --i) g[i] = g[i + 1 ] * nums[i + 1 ];
int pre = 1 ;
for (int i = 0 ; i < n; ++i) {
g[i] *= pre;
pre *= nums[i];
}
return g;
}
}
5. 和为 k 的子数组 给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。
输入:nums = [1, 1, 1], k = 2
输出:2
解释:共有两个子数组的和为 2,分别是 [1, 1] 和 [1, 1]。
输入:nums = [1, 2, 3], k = 3
输出:2
解释:共有两个子数组的和为 3,分别是 [1, 2] 和 [3]。
1 <= nums.length <= 2 * 10^4
-1000 <= nums[i] <= 1000
-10^7 <= k <= 10^7
根据题目要求,我们需要求出和为 K 的子数组的个数。
我们可以使用暴力解法,定义两个指针 i 和 j,固定 i,j 依次往后遍历,注意,找到符合的数组后,j 也要继续遍历,因为以 i 为起点的和为 K 的子数组不止一个,j 遍历完一遍数组后 i 往后加一,直到 i 遍历完数组,这样就求出和为 K 的子数组的个数,但是暴力解法的时间复杂度是 O(n^2),一定会超时。
对于这道题,我们不能使用滑动窗口,当窗口内出现符合的子数组时,统计个数后窗口的左边端点会前移,因为数组的元素有正有负不满足单调性,这样是会错过一些符合的数组,比如在 [1, -1, 0, 0, 0] 求和为 0 的子数组的个数。
我们的最优解法是前缀和 + 哈希表。对于 [0, i] 区间的数组,其元素之和为 sum,如果通过哈希表查询到该区间数组和为 sum - K 的子数组的个数,这样就间接知道了 [0, i] 区间的数组内和为 K 的子数组的个数。
class Solution {
public int subarraySum (int [] nums, int k) {
Map<Integer, Integer> hash = new HashMap <>();
hash.put(0 , 1 );
int sum = 0 , ret = 0 ;
for (int i = 0 ; i < nums.length; ++i) {
sum += nums[i];
if (hash.containsKey(sum - k)) ret += hash.get(sum - k);
hash.put(sum, hash.getOrDefault(sum, 0 ) + 1 );
}
return ret;
}
}
对于前缀和,我们没有必要格外开辟空间来计算,只需要用一个变量 sum 去维护即可。
hash.put(0, 1):和为 0 的子数组出现的次数默认有一次,这是因为区间元素和刚好第一次为 K 时,按照我们的算法,需要查找和为 0 的子数组个数,但是由于整个区间的所有元素和为 K,没有额外剩余的元素去构成和为 0 的子数组,所以如果没有 hash.put(0, 1),哈希表是查找不到和为 0 的子数组的个数,我们的算法是会遗漏整个区间的和第一次为 K 的情况,最后统计出来的和为 K 的子数组的个数是会少一个的。
时间复杂度 :O(n)
空间复杂度 :O(n),哈希表的空间开销
6. 和可被 k 整除的子数组 给定一个整数数组 nums 和一个整数 k,返回其中元素之和可被 k 整除的(连续、非空)子数组的数目。
输入:nums = [4, 5, 0, -2, -3, 1], k = 5
输出:7
解释:共有 7 个子数组满足其元素之和可被 k = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
输入:nums = [5], k = 9
输出:0
1 <= nums.length <= 3 * 10^4
-10^4 <= nums[i] <= 10^4
2 <= k <= 10^4
对于这道题,我们依然使用前缀和 + 哈希表 ,但是在此之前先引入下同余定理。
如果 (a - b) % K == 0,则有 a % K == b % K。
所以整体思路就是对于 [0, i] 区间的数组,其元素之和为 sum,对 sum 进行取模 (% K) 余数为 mod,如果通过哈希表查询到该区间内数组和的余数同样为 mod 的子数组的个数,这样就间接知道了 [0, i] 区间的数组内和可为 K 整除的子数组的个数。
class Solution {
public int subarraysDivByK (int [] nums, int k) {
Map<Integer, Integer> hash = new HashMap <>();
hash.put(0 , 1 );
int ret = 0 , sum = 0 ;
for (int e : nums) {
sum += e;
int mod = (sum % k + k) % k;
if (hash.containsKey(mod)) ret += hash.get(mod);
hash.put(mod, hash.getOrDefault(mod, 0 ) + 1 );
}
return ret;
}
}
哈希表存储的是前缀和的余数及其出现的次数,与上道题不同。
hash.put(0, 1):余数为 0 默认出现次数为 1 次,是为了处理区间元素和第一次刚好被 K 整除的特殊情况,防止答案遗漏。
(sum % k + k) % k:在 Java/C++ 中,余数可以为负数,需要将其调整为正数,不然会影响结果。
时间复杂度 :O(n)
空间复杂度 :O(K),哈希表存储余数的额外空间开销。
7. 连续数组 给定一个二进制数组 nums,找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
输入:nums = [0, 1]
输出:2
说明:[0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
输入:nums = [0, 1, 0]
输出:2
说明:[0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。
1 <= nums.length <= 10^5
nums[i] 不是 0 就是 1
题目要求我们找含有相同数量的 0 和 1 的最长连续子数组的长度,我们可以将 0 看作成 -1,这样问题就转换成了求和为 0 的子数组最长长度。
解决这道题的思路依然是前缀和 + 哈希 ,对于 [0, i] 区间的数组,其元素之和为 sum,通过哈希表查询到该区间内数组和同样为 sum 的子数组的末尾元素下标,间接求出和为 0 的子数组长度。
但是需要注意的是,当哈希表中已经存在 sum 时,不必更新 sum 在哈希表中的下标,因为题目要求的是子数组最长长度。
class Solution {
public int findMaxLength (int [] nums) {
Map<Integer, Integer> hash = new HashMap <>();
hash.put(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;
}
}
哈希表存储 [0, i] 区间元素和及其下标 i。
hash.put(0, -1):前缀和为 0 的子数组下标默认为 -1,处理前缀和第一次为 0 的特殊情况。
时间复杂度 :O(n)
空间复杂度 :O(n),哈希表的额外空间开销。
8. 矩阵区域和 给你一个 m x n 的矩阵 mat 和一个整数 k,请你返回一个矩阵 answer,其中每个 answer[i][j] 是所有满足以下条件的元素 mat[r][c] 的和:
i - k <= r <= i + k
j - k <= c <= j + k
(r, c) 在矩阵内。
输入:mat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]], k = 1
输出:[[12, 21, 16], [27, 45, 33], [24, 39, 28]]
输入:mat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]], k = 2
输出:[[45, 45, 45], [45, 45, 45], [45, 45, 45]]
m == mat.length
n == mat[i].length
1 <= m, n, k <= 100
1 <= mat[i][j] <= 100
通过分析题目可以知道,其实就是快速求出子矩阵 [i - k, j - k] 到 [i + k, j + k] 的所有元素的和,我们可以先预处理出来一个矩阵前缀和数组 dp,然后通过矩阵前缀和数组快速求出子矩阵的所有元素和。
构建矩阵前缀和 dp
开辟空间时多开一行和一列,简化边界情况的处理
dp[i][j] 表示矩阵 mat 从 [0, 0] 到 [i - 1, j - 1] 的所有元素和。
通过 dp 快速求子矩阵的和
由于矩阵前缀和的下标与矩阵 mat 并不是一一对应的,我们在计算子矩阵的起始位置与末尾位置的下标时要进行转换。
根据题目可知,我们通过 i 和 j 求子矩阵的坐标时是可能会越界,我们要特殊处理。
sum = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1]
class Solution {
public int [][] matrixBlockSum(int [][] mat, int k) {
int m = mat.length, n = mat[0 ].length;
int [][] dp = new int [m + 1 ][n + 1 ];
for (int i = 1 ; i < m + 1 ; ++i)
for (int j = 1 ; j < n + 1 ; ++j)
dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ] - dp[i - 1 ][j - 1 ] + mat[i - 1 ][j - 1 ];
int [][] ret = new int [m][n];
for (int i = 0 ; i < m; ++i) {
int x1 = Math.max(0 , i - k) + 1 ;
int x2 = Math.min(m - 1 , i + k) + 1 ;
for (int j = 0 ; j < n; ++j) {
int y1 = Math.max(0 , j - k) + 1 ;
int y2 = Math.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;
}
}
矩阵前缀和 dp 与矩阵 mat 的下标不是一一对应的,比如 dp[1][1] 对应原数组 mat[0][0],计算矩阵前缀和要留意。
通过 i 与 j 求子矩阵的坐标时要防止越界并且转换成与 dp 数组对应,因为要通过 dp 求出子矩阵的所有元素和。
时间复杂度 :O(m × n)
空间复杂度 :O(m × n),矩阵前缀和数组 dp 的额外空间开销
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online