跳到主要内容
前缀和算法详解:高效解决区间求和问题 | 极客日志
C++ 算法
前缀和算法详解:高效解决区间求和问题 综述由AI生成 前缀和是一种通过预处理将区间查询复杂度降为 O(1) 的高效算法。本文详细讲解了一维及二维前缀和的构建原理与状态转移方程,重点分析了如何处理边界条件及负数取模等细节。结合 LeetCode 典型例题,如中心下标、除自身以外乘积、和为 K 的子数组等,展示了前缀和配合哈希表、容斥原理的实际应用技巧。内容涵盖 C++ 代码实现,旨在帮助读者深入理解该算法模式并提升解题效率。
忘忧 发布于 2026/3/22 更新于 2026/4/25 1 浏览前缀和算法详解:高效解决区间求和问题
在算法竞赛和实际开发中,频繁进行数组区间求和是常见需求。如果直接遍历计算,每次查询的时间复杂度为 O(n),当查询次数 q 很大时,总复杂度会达到 O(nq),性能瓶颈明显。前缀和(Prefix Sum)通过预处理将查询时间降至 O(1),是处理此类问题的核心技巧。
一维前缀和基础
原理与递推
前缀和的核心思想是构建一个辅助数组 dp,其中 dp[i] 存储原数组从起始位置到 i 的所有元素之和。这样,任意区间 [l, r] 的和就可以通过 dp[r] - dp[l-1] 快速得出。
递推公式非常简单:
dp[i] = dp[i-1] + arr[i]
实现细节
在实际编码中,为了处理边界情况(特别是当 l=1 时 l-1=0),通常会将数组下标从 1 开始,并设置 dp[0] = 0。这相当于增加了一个虚拟的辅助节点,避免了判断 l-1 < 0 的逻辑分支。
#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 > dp (n + 1 ) ;
for (int i = 1 ; i <= n; i++)
dp[i] = dp[i - ] + arr[i];
l = , r = ;
(q--){
cin >> l >> r;
cout << dp[r] - dp[l - ] << endl;
}
;
}
1
int
0
0
while
1
return
0
注意 :这里 dp[0] 默认为 0,正好对应区间 [1, 1] 减去空集的情况,逻辑非常自洽。
二维前缀和扩展 对于矩阵区域求和问题,思路与一维类似,但需要处理行和列两个维度。
状态定义与转移 设 dp[i][j] 表示从 (1, 1) 到 (i, j) 矩形区域内所有元素的和。根据容斥原理,当前格子的值等于上方、左方区域之和,减去重复计算的左上角区域,再加上当前格子的值:
dp[i] [j] = dp[i-1] [j] + dp[i] [j-1] - dp[i-1] [j-1] + arr[i] [j]
区域查询 查询以 (x1, y1) 为左上角,(x2, y2) 为右下角的矩形和时,同样利用容斥原理:
Sum = dp[x2] [y2] - dp[x1-1] [y2] - dp[x2] [y1-1] + dp[x1-1] [y1-1]
#include <iostream>
#include <vector>
using namespace std;
int main () {
int n = 0 , m = 0 , q = 0 ;
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] = dp[i - 1 ][j] + dp[i][j - 1 ] + arr[i][j] - 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;
}
return 0 ;
}
经典应用场景 理解了模板后,关键在于如何灵活变形。以下是几个高频考点。
寻找数组的中心下标 (LeetCode 724) 题目要求找到一个索引,使得其左侧元素之和等于右侧元素之和。我们可以分别构建前缀和数组和后缀和数组,或者更巧妙地,只维护一个总和,遍历时动态计算左侧和。
class Solution {
public :
int pivotIndex (vector<int >& nums) {
int total = 0 ;
for (int num : nums) total += num;
int leftSum = 0 ;
for (int i = 0 ; i < nums.size (); i++) {
if (leftSum == total - leftSum - nums[i])
return i;
leftSum += nums[i];
}
return -1 ;
}
};
除自身以外数组的乘积 (LeetCode 238) 这道题是前缀积的应用。为了避免除法,我们分别计算每个位置左侧的乘积和右侧的乘积,然后相乘。
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 ;
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 ];
vector<int > ret (n) ;
for (int i = 0 ; i < n; i++)
ret[i] = f[i] * g[i];
return ret;
}
};
和为 K 的子数组 (LeetCode 560) 暴力枚举子数组起点和终点是 O(n²)。利用前缀和,问题转化为:寻找有多少对 (j, i) 满足 prefix[i] - prefix[j] = k,即 prefix[j] = prefix[i] - k。
我们可以用哈希表记录每个前缀和出现的次数。遍历过程中,检查 sum - k 是否在表中,累加次数即可。
class Solution {
public :
int subarraySum (vector<int >& nums, int k) {
unordered_map<int , int > hash;
int sum = 0 , ret = 0 ;
hash[0 ] = 1 ;
for (auto x : nums){
sum += x;
if (hash.count (sum - k))
ret += hash[sum - k];
hash[sum]++;
}
return ret;
}
};
和可被 K 整除的子数组 (LeetCode 974) 基于同余定理,若 (a - b) % k == 0,则 a % k == b % k。因此只需统计相同余数的前缀和数量。
注意 :C++ 中负数取模可能为负,需修正为 (sum % k + k) % k。
class Solution {
public :
int subarraysDivByK (vector<int >& nums, int k) {
unordered_map<int , int > hash;
hash[0 % k]++;
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;
}
};
连续数组 (LeetCode 525) 将 0 视为 -1,1 视为 1。问题转化为寻找和为 0 的最长子数组。此时哈希表存储的是 <前缀和,最早出现下标>,用于计算最大长度。
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;
}
};
矩阵区域和 (LeetCode 1314) 这是二维前缀和的直接应用,但增加了边界限制 k。需要计算以 (i, j) 为中心,边长为 2k+1 的区域和。注意坐标映射和边界裁剪。
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][j-1 ] + dp[i-1 ][j] - dp[i-1 ][j-1 ] + mat[i - 1 ][j - 1 ];
vector<vector<int >> answer (m, vector <int >(n));
for (int i = 0 ; i < m; i++)
for (int j = 0 ; j < n; j++){
int x1 = max (i - k, 0 ) + 1 ;
int y1 = max (j - k, 0 ) + 1 ;
int x2 = min (i + k, m - 1 ) + 1 ;
int y2 = min (j + k, n - 1 ) + 1 ;
answer[i][j] = dp[x2][y2] - dp[x2][y1 - 1 ] - dp[x1 - 1 ][y2] + dp[x1 - 1 ][y1 - 1 ];
}
return answer;
}
};
前缀和看似简单,但其变体众多。掌握核心递推关系和容斥原理,就能应对绝大多数区间统计问题。在实际编写时,务必注意数组越界和整数溢出风险。
相关免费在线工具 加密/解密文本 使用加密算法(如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