1. 前缀简介
在算法中,前缀和通常用于处理与数组相关的区间查询问题。它的核心思想是预先计算出一个数组的前缀和数组,然后通过对前缀和数组的查询,可以快速得出某一段连续区间的和。
前缀和的定义
给定一个数组 arr,前缀和数组 prefixSum 的定义是:
prefixSum[ i ] = arr[0] + arr[1] + ... + arr[i]
(即数组 arr
从第 0 个元素到第 i
个元素的和)
前缀和的核心思想
通过事先计算好前缀和数组,你可以快速求出任意区间的和。例如,想要计算 arr[q]
到 arr[r]
的区间和,使用前缀和数组,可以通过以下公式直接得到:
sum(arr[q...r]) = prefixSum[r] - prefixSum[q-1]
如果q == 0,则 prefixSum[r]
本身就是所需的区间和。
前缀和的优势
- 预处理时间: 计算前缀和数组的时间复杂度为
O(n)
,其中n
是数组的大小。 - 查询时间: 一旦计算出前缀和数组,任意区间和的查询时间复杂度为
O(1)
,这对于多次查询非常高效。 - 时间复杂度:
- 预处理:
O(n)
- 查询区间和:
O(1)
- 预处理:
2.模版前缀和
1.题目描述
2.算法思想
(1)预处理出来一个前缀和数组
使用数组 dp 来存储前缀和, dp[i] 表示 nn[1] 到 nn[i] 的和。
(2)使用前缀和数组
对于每次查询,给定一个区间 [l, r]
,要求区间和。利用前缀和数组的性质,区间和可以通过 dp[r] - dp[l - 1]
来求得。
dp[r]
表示从 nn[1]
到 nn[r]
的和。dp[l - 1]
表示从 nn[1]
到 nn[l - 1]
的和。因此,dp[r] - dp[l - 1]
即为从 nn[l]
到 nn[r]
的区间和。
3.代码实现
#include <iostream>
#include <vector>
using namespace std;
int main() {
//输入
int n, q;
cin >> n >> q;
vector<int> nn(n + 1);
for (int i = 1; i <= n; i++) cin >> nn[i];
//预处理出来一个前缀和数组
vector<long long> dp(n + 1);
for(int i = 1; i <= n; i++) dp[i] = dp[i - 1] + nn[i];
//使用前缀和数组
int l = 0, r = 0;
while (q--)
{
cin >> l >> r;
cout << dp[r] - dp[l - 1] << endl;
}
return 0;
}
3. 模板二维前缀和
1.题目描述
2.算法思想
(1)预处理出来一个前缀和数组
使用数组 dp 来存储前缀和, 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]
(2)使用前缀和数组
对于每次查询 (x1, y1)
到 (x2, y2)
sum = dp[x2][y2] − dp[x2][y1−1] − dp[x1−1][y2] + dp[x1−1][y1−1]
3.代码实现
#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, 0));
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, 0));
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] - dp[i - 1][j - 1] + arr[i][j];
}
}
//使用前缀和矩阵
int x1, x2, y1, y2;
while(q--)
{
cin >> x1 >> y1 >> x2 >> y2;
cout << dp[x2][y2] - dp[x2][y1 - 1] - dp[x1 - 1][y2] + dp[x1 - 1][y1 - 1] << endl;
}
return 0;
}
4.寻找数组的中心下标
1.题目描述
2.算法思想
(1)预处理出来一个前缀和、一个后缀和数组
使用数组 f 来存储前缀和, f[i] 表示从 nums[0] 到 nums[i - 1] 的元素和。
使用数组 g 来存储后缀和, g[i] 表示从 nums[i + 1] 到 nums[n - 1] 的元素和。
(2)使用前缀和、后缀和数组
如果某个 i
满足 f[i] == g[i]
,则该位置 i
就是中心下标。
3.代码实现
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n = nums.size();
//前缀和数组、后缀和数组
vector<int> f(n);
vector<int> 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;
}
};
5.除自身以外数组的乘积
1.题目描述
2.算法思想
(1)预处理出来前缀积和后缀积数组
前缀积:f[i]
表示 nums[0]
到 nums[i-1]
的元素积。
后缀积: g[i]
表示 nums[i+1]
到 nums[n-1]
的元素积。
(2)使用前缀积和后缀积数组
对于每个索引 i
,最终的结果 ret[i]
通过 f[i] * g[i]
得到,其中 f[i]
是左边所有元素的积,g[i]
是右边所有元素的积。
3.代码实现
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, 1), 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];
//2.使用
vector<int> ret(n);
for(int i = 0; i < n; i++)
{
ret[i] = f[i] * g[i];
}
return ret;
}
};
6.和为k的子数组
1.题目描述
2.算法思想
(1)预处理出来一个前缀和数组
使用数组 dp 来存储前缀和, dp[i] 表示 nn[1] 到 nn[i] 的和。
(2)使用前缀和数组
3.代码实现
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 e : nums)
{
sum += e;//计算当前位置的前缀和
if(hash.count(sum - k)) ret += hash[sum - k];//统计个数
hash[sum]++;//保存出现过的前缀和
}
return ret;
}
};
. - 力扣(LeetCode). - 力扣(LeetCode)
7.和可被k整除的数组
1.题目描述
2.算法思想
(1)预处理出来一个前缀和数组
sum += e
:计算当前的前缀和。
int r = (sum % k + k) % k
:计算当前前缀和对 k
的余数,并保证余数为非负数。如果 sum % k
为负数,(sum % k + k)
会将其转化为正数,再进行 % k
操作。
(2)使用前缀和数组
if (hash.count(r)) ret += hash[r];
:如果当前余数 r
在哈希表中已经出现过,那么意味着存在一些子数组的和是 k
的倍数(通过当前前缀和与之前某个前缀和之间的差值可以整除 k
)。因此,我们将 hash[r]
加到结果 ret
中。
3.代码实现
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int, int> hash;
hash[0] = 1; // 初始化:前缀和为 0 出现 1 次,表示空子数组
int sum = 0, ret = 0; // sum 表示当前前缀和,ret 是结果计数
for (int e : nums) {
sum += e; // 累加当前元素
int r = (sum % k + k) % k; // 计算前缀和对 k 的余数,保证余数非负
// 如果该余数 r 曾经出现过,那么说明有某些子数组的和是 k 的倍数
if (hash.count(r)) ret += hash[r]; // 计算结果
// 将当前余数 r 的出现次数增加 1
hash[r]++;
}
return ret; // 返回符合条件的子数组个数
}
};
8.连续数组
1.题目描述
2.算法思想
前缀和 + 哈希表
前缀和 sum
表示从数组的第一个元素到当前元素的和。此时,sum
可能是负数、零或正数。关键点是:当 sum
相同时,说明两者之间的子数组的 0
和 1
数量相等。
使用哈希表 hash
来存储每个前缀和 sum
对应的索引。如果当前的前缀和 sum
已经出现过,那么意味着从上一次出现该前缀和的位置到当前位置之间的子数组中,0
和 1
数量相等。此时,子数组的长度就是 i - hash[sum]
,即从 hash[sum]
到当前索引 i
的距离。
如果当前前缀和 sum
没有出现过,则将其存储在哈希表中,表示这个前缀和首次出现的位置。
ret = max(ret, i - hash[sum]);
:每次发现前缀和相同,就更新最长子数组的长度。
3.代码实现
class Solution {
public:
int findMaxLength(vector<int>& nums) {
unordered_map<int, int> hash;
hash[0] = -1;//默认有一个前缀和为0的情况
int sum = 0, ret = 0, n = nums.size();
// 通过将 0 转换为 -1,便于后续计算
for (int i = 0; i < n; i++)
{
if(nums[i] == 0) nums[i] = -1;
}
for (int i = 0; i < n; i++)
{
sum += nums[i];//计算当前位置的前缀和
if (hash.count(sum)) ret = max(ret, i - hash[sum]);// 计算最大长度
else hash[sum] = i;// 存储当前前缀和及其对应的下标
}
return ret;
}
};
9.矩阵区域和
1.题目描述
2.算法思想
(1)预处理出来一个前缀和数组
使用数组 dp 来存储前缀和, dp[i][j] 表示 mat[0][0]到 mat[i - 1][j - 1] 的和。
(2)使用前缀和数组
首先找到满足mat[r][c]的最大坐标和最小坐标。
通过下面公式即可满足要求:
answer[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1];
3.代码实现
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));
//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] + mat[i - 1][j - 1] - dp[i - 1][j - 1];
//2.使用
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(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;
answer[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1];
}
return answer;
}
};