目录
[DP34 【模板】前缀和](#DP34 【模板】前缀和)
[DP35 【模板】二维前缀和](#DP35 【模板】二维前缀和)
[和为 K 的子数组](#和为 K 的子数组)
[和可被 K 整除的子数组](#和可被 K 整除的子数组)
前缀和:快速求出数组中某一个连续区间的和。
DP34 【模板】前缀和

思路:
- 先预处理出来一个前缀和数组,命名为 dp。
- dp[i] 表示的是 [1, i] 区间内所有元素的和。
- dp[i] = dp[i - 1] + arr[i](arr 是题中给出的数组)
- 使用前缀和数组快速求出查询区间的元素和即可。
**细节问题:**为什么数组下标要从1开始使用? 因为如果数组从下标 0 开始使用,当 l = 0 时,dp[l - 1] 访问的是 dp[ -1],这里访问越界了,需要特殊处理这种情况才行,但是从1开始使用,让dp[0] = 0,就不会出现这种特殊情况了。
代码:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n, q;
cin >> n >> q;
vector<long long> dp(n + 1, 0);
for(int i = 1; i <= n; i++){
int tmp;
cin >> tmp;
dp[i] = dp[i - 1] + tmp;
}
int l, r;
while(q--){
cin >> l >> r;
cout << dp[r] - dp[l - 1] << endl;
}
return 0;
}
DP35 【模板】二维前缀和

思路:
-
预处理一个前缀和矩阵,命名为dp。
- dp[ i ][ j ] 表示:从 [ 1 ][ 1 ] 位置到 [ i ][ j ]位置的,这段区间里面所有元素的和。

-
使用前缀和矩阵。(S 为图中 D 区域的元素和)
代码:
cpp
#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][j - 1] + dp[i - 1][j] + arr[i][j] - dp[i - 1][j - 1];
int x1 = 0, x2 = 0;
int y1 = 0, y2 = 0;
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;
}
}
寻找数组的中心下标

思路:
- 定义一个前缀和数组 f,一个后缀和数组 g。
- f[i] 表示:[0, i - 1] 区间,所有元素的和。
- g[i] 表示:[i + 1, n - 1] 区间,所有元素的和,n 是数组元素个数。
- 比较前缀和数组和后缀和数组中哪个位置的值相等,哪个位置就是中心下标。
代码:
cpp
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, 0);
vector<int> g(n, 0);
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;
}
};
除了自身以外数组的乘积

思路:
- 定义一个前缀和数组 f,一个后缀和数组 g。
- f[i] 表示:[0, i - 1] 区间,所有元素的乘积。
- g[i] 表示:[i + 1, n - 1] 区间,所有元素的乘积,n 是数组元素个数。
- 将前缀和数组和后缀和数组第 i 个位置的元素相乘就是该位置除自身以外数组的乘积。
- 注意:f[0] = g[n - 1] = 1,nums 中 0 左侧和 n - 1 右侧都没有元素了,所以前缀和数组和后缀和数组中这两个位置赋值 1 才不会影响计算。
代码:
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, 1);
vector<int> g(n, 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 的子数组

思路:前缀和+哈希表
前缀和数组 sum[i] 表示:[0, i] 区间内所有元素之和。前缀和数组作用:

根据上图可以看出,只需要找到 i 前面有多少前缀和等于 sum[i] - k,就有多少以 i 为结尾并且元素总和为 k 的连续子数组。
哈希表的作用:映射前缀和和该前缀和的个数,方便快速找到 i 位置前有多少和为 sum[i] - k 的子数组。
细节问题:
- 不要一次性将所有前缀和都放入哈希表中再去查找哈希表,因为都放进入后,找 i 位置之前有多少和为 sum[i] - k 的子数组时会被 i 位置后面的前缀和影响,结果就会不对。
- 不需要真的创建一个前缀和数组,因为哈希表中已经将前缀和以及对应的个数映射出来了,只需要创建一个变量保存当前位置的前缀和方便计算后续位置的前缀和即可。
- 如果 i 位置的前缀和等于 k,那么 sum[i] - k = 0,此时 [0, i] 这个区间表示的子数组是符合要求的,但是查哈希表的时候会忽略这种情况,导致少计算一种情况,所以一开始就向哈希表中丢入一个 <0, 1> 的映射,防止忽略这种情况。
代码:
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int sum = 0;
unordered_map<int, int> hash;
hash[0] = 1;
int ret = 0;
for(int i = 0; i < nums.size(); i++){
sum += nums[i];
int tmp = sum - k;
if(hash.count(tmp)) ret += hash[tmp];
hash[sum]++;
}
return ret;
}
};
和可被 K 整除的子数组

思路:前缀和 + 哈希表
补充知识:
- 同余定理:当(a - b)/ p = k ....0 (整除...余0) 成立时,可以推导出 a % p == b % p。
- C++中,[负数 % 正数] 的结果是负数,通过以下式子修正为正数:a % p + p(a是负数),考虑到实际情况中 a 还有可能是正数,如果是正数取模就可以了,没必要加 p,所以将式子进行如下修改来统一正负数:(a % p + p)% p。

根据上图所示,我们要找 i 位置为结尾的子数组中有多少元素之和能被 k 整除,其实就是找 i 前面有多少前缀和与 sum 取模 k 后同余。为了快速找到 i 前面有多少前缀和与 sum 同余,我们使用哈希表存储 i 位置前面所有前缀和取模 k 后的余数和这个余数个数的映射即可解决问题。这道题的思路和上一道题类似,只不过还需要用到同余定理和负数取模之后的修正。
细节问题和上题一样,这里不赘述。
代码:
cpp
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int, int> hash;
hash[0] = 1;
int sum = 0;
int ret = 0;
for(int i = 0; i < nums.size(); i++){
sum += nums[i];
int tmp = (sum % k + k) % k;
if(hash.count(tmp)) ret += hash[tmp];
hash[tmp]++;
}
return ret;
}
};
连续数组

**思路:**将数组中所有的 0 都修改为-1, 这道题就转化为了在数组中,找出最长的子数组,使子数组中所有元素和为 0,与和为 k 的子数组那道题思路类似,只不过 k 固定为 0,并且求的是数组长度而不是个数。
细节问题:
- 因为求的是满足条件的数组的最大长度,所以哈希表存储的是前缀和 和 下标的映射,下标是为了方便数组长度的计算。
- 如果有重复的前缀和 和 下标的映射,保留前面的即可。因为子数组右端点固定的情况下,左端点越往左,数组长度越长,而前缀和相同的情况下,并且还是从左向右算前缀和,先存储在哈希表中的下标更靠左,所以保留前面的。
- 当 [0, i] 区间整个都符合的情况下,即 sum[ i ] = 0,这时根据前缀和+哈希表的解题方式,理论上需要在下标 -1 处有一个前缀和等于 0 才行,不然这种情况会被漏掉,所以一开始我们就将这种映射直接加入到哈希表中,防止遗漏这种情况导致结果不正确。

代码:
cpp
class Solution {
public:
int findMaxLength(vector<int>& nums) {
unordered_map<int, int> hash;
hash[0] = -1;
int sum = 0;
int 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;
}
};
矩阵区域和

思路: 这道题其实就是在求给出数组中每个元素向上下左右分别拓展了 k 个单位之后的矩阵的矩阵和,并把这些矩阵和组成一个新的矩阵。使用求二维前缀和的方式就可以(详细看本文第二题)。
细节问题:
-
如何求计算二维矩阵元素和需要的两个坐标以及避免越界情况:
-
下标的映射:为了避免处理边界情况,存放二维前缀和的 dp 数组空间多开一行一列,这样我们使用下标的时候就可以从 1(行,列都是)开始使用,不用管是否会数组越界。否则计算二维前缀和 和 使用二维前缀和数组的时候总需要考虑边界情况(是否越界)。同时因为 dp 数组多开了一行一列,但是多开的这一行一列并没有使用,实际使用是从 [1, 1] 开始的,所以使用 dp 数组的位置映射原数组的位置时需要减1,使用原数组的位置映射 dp 数组的位置时需要加 1。
代码:
cpp
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
int m = mat.size();
int 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;
int y1 = max(0, j - k) + 1;
int x2 = min(m - 1, i + k) + 1;
int 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;
}
};


