前缀和
1.(模板)一维前缀和

前缀和,能快速求出数组arr中某一个连续区间的和 。设arr=1,4,7,2,5,8,3,6,9。创建前缀和数组dp,dp中每个元素dpi为arr在1,i区间的元素和。即 dp[i]=dp[i-1]+arr[i] 。数组的下标应从1开始,调用vector创建数组时要开n+1个空间,arr0和dp0都会被初始化为0 。

求arr中l,r的元素和。如下图所示,先得到1~r的元素和dpr, 再得到1~l-1的元素和dpl-1,二者做差即l,r的元素和。
即l~r元素和 = dp[r] - dp[l-1] 。
若下标从0开始,如果求0,2元素和,则dp2-dp-1,此时要处理边界问题,不然会越界访问。下标从1开始则不会有这个问题,例如求1,2元素和,则dp2-dp0,dp0为0,元素和即为dp2 。

根据题意,查询m次元素和,直接遍历,即按照描述操作,时间复杂度为O(m*n),若用前缀和,时间复杂度为O(m+n) 。
cpp
#include <iostream>
#include<vector>
using namespace std;
int main()
{
int n=0,m=0;
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;
}
2.(模板)二维前缀和

下标从1开始的矩阵arr,给左上角元素arrx1 y1,右下角元素arrx2 y2,求这个范围内的元素和。

开一个与arr同样规模的前缀和矩阵dp,dpi j为从arr1 1到arri j的元素和。

dpi j = A区域元素和 + B区域元素和 + C区域元素和 + D 。即dpi j = (A+B)+(A+C)+D-A。
即dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] - dp[i-1][j-1] 。
对于给定左上角元素arrx1 y1,右下角元素arrx2 y2,求这个范围内的元素和。

即求D区域前缀和,D区域前缀和 = 1,1到x2,y2前缀和 - 1,1到x1-1,y2前缀和 - 1,1到x2,y1-1前缀和 + 1,1到x1-1,y1-1前缀和。即D = (A+B+C+D) - (A+B) - (A+C) + A 。
即[x1,y1]到[x2,y2]前缀和 =dp[x2,y2] - dp[x1-1,y2] - dp[x2,y1-1] + dp[x1-1][y1-1] 。
查询q次,arr为nm矩阵,直接暴力解法的时间复杂度为O(n * m * q),前缀和时间复杂度为O(q + nm)。
cpp
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;
}
3.寻找数组的中心下标

题目要求返回一个下标 i ,满足0 ~ i-1的元素和等于 i+1 ~ n-1的元素和。使用前缀和时不一定要套模板,关键是能仿照前缀和的思想解题。之前的前缀和dpi是下标从1开始到 i 的元素和,现在可以定义前缀和 fi 为下标从0开始到 i-1 的元素和。即 fi = fi-1+numsi-1。

同样可以定义后缀和 gi 为 i+1 ~ n-1的元素和。即 gi = gi+1 + numsi+1。

f0为下标为0的元素之前的元素和,对于数组nums是越界的,所以规定 f0=0;同理gn-1也会越界访问nums,所以规定 gn-1=0 。
cpp
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;
}
};
4.除自身以外数组的乘积

与上一道题类似,参照前缀和的思想,定义与上一题类似的前缀积、后缀积,注意处理边界情况。
cpp
class Solution3 {
public:
vector<int> productExceptSelf(vector<int>& nums)
{
int n = nums.size();
vector<int> f(n), g(n), answer(n);
//预处理前缀积
f[0] = 1;
for (int i = 1;i < n;i++)
f[i] = f[i - 1] * nums[i - 1];
//预处理后缀积
g[n - 1] = 1;
for (int i = n - 2;i >= 0;i--)
g[i] = g[i + 1] * nums[i + 1];
for (int i = 0;i < n;i++)
answer[i] = f[i] * g[i];
return answer;
}
};
5. 和为k的子数组

题目要求返回和为k的子数组的个数,numsi是可以为负数的,所以不能用滑动窗口。如果子数组j,i元素和为k,i的前缀和为sum,则j的前缀和为sum-k。

题目可以转换成 i 的前缀和为sum,记录0到 i-1 的每个前缀和,如果0到 i-1 中存在前缀和为sum-k,就相当于找到一个符合要求的子数组。用哈希表记录前缀和,key为前缀和,value为 值为key的前缀和个数。前缀和为sum-k,个数ret+=hashsum-k。
不必再创建一个前缀和数组,遍历nums,用sum记录前缀和,哈希表保存sum。如果nums0==k,则一开始sum为k,sum-k就为0,所以要预处理hash0=1。得到0到 i 前缀和sum,先调用hash.count查找是否有前缀和满足sum-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 (auto e : nums)
{
sum += e;
if (hash.count(sum - k))
ret += hash[sum - k];
hash[sum]++;
}
return ret;
}
};
6. 和可被k整除的子数组(同余定理)

与上一道题类似。先补充同余定理和C++、Java关于 负数%正数。
同余定理:(a-b)/p = k ... 0可得出a%p = b%p。证明:因为(a-b)/p=k,所以a-b=kp,即a=b+kp。a%p的本质是a一直减p ,直到第一次结果x小于p,x即为a/p的余数,即x=a%p。又因为a=b+kp,同时取模,即a%p=(b+kp)%p,则(b+kp)%p即b%p,即a%p=b%p。
C++、Java中 负数%正数得到负数,写代码时要修正,无论a为正数还是负数,a%p都写成(a%p+p)%p,结果就都为正数。
nums中,j,i的元素和 % k==0,则 ( i的前缀和 - j的前缀和 )%k == 0,根据同余定理,i的前缀和 % k == j的前缀和 % k。设 i 的前缀和为sum,只要 i 之前存在前缀和x,满足 x%k == sum%k,即找到了符合要求的子数组。
创建unordered_map<int,int> hash,key为 前缀和%k所得到的余数,value为该余数的个数。如果nums0%k==0,则一开始sum除以k的余数为0,预处理hash0=1。

cpp
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k)
{
unordered_map<int, int> hash;
hash[0] = 1;//0的余数
int sum = 0, ret = 0;
for (auto e : nums)
{
sum += e;//当前位置的前缀和
int r = (sum % k + k) % k;
if (hash.count(r))
ret += hash[r];//统计结果
hash[r]++;
}
return ret;
}
};
7.连续数组

题目要求找到0和1个数相同的最长子数组,把0改成-1,题目就转化成找到和为0的最长子数组,即在nums中,在0,i中找到0,j的元素和为sum。与和为k的子数组类似。

创建unordered_map<int,int> hash,key为前缀和,因为要求出长度,所以value为下标。先得到前缀和sum,调用hash.count,在哈希表中找sum,计算出长度。
如何计算长度?i-j。

hash0=-1,当前缀和sum首次为0时,如果下标为0,则长度 i-0=i,而长度应该为 i+1。

只有在hash.count未找到前缀和时,才需要把前缀和及其下标插入哈希表。如果得到的每一个<sum,i >都插入哈希表,就会导致上图的 j 右移,i-j的长度不是最长。

如果不这样做,sum为0时,就会从<0,-1>变成<0,1>,最终i为3时,i-(-1)变成 i-1,长度变短。
cpp
class Solution {
public:
int findMaxLength(vector<int>& nums)
{
int n = nums.size();
unordered_map<int, int> hash;
hash[0] = -1;
int sum = 0, ret = 0;
for (int i = 0;i < n;i++)
{
sum += nums[i] == 0 ? -1 : 1;
if (hash.count(sum))
ret = max(ret, i - hash[sum]);
else
hash[sum] = i;
}
return ret;
}
};
8.矩阵区域和

题目的意思是返回一个与mat同样规模的矩阵ans,ansi j为mati j向四周扩展k范围的元素和,下图k=1,ans0 0 = mat0 0+mat0 -1+mat0 1 + mat-1 0+mat1 0 + mat-1 -1+mat-1 1+mat1 -1+mat1 1 = 12 。

这道题显然用二维前缀和来解决。创建前缀和矩阵dp,mat下标是从0开始的,所以往dp填入数据的公式应为:
dp[i][j] = dp[i][j-1] + dp[i-1][j] - dp[i-1][j-1] + mat[i-1][j-1]

ans中的数据本质是mat中的元素和,得到mat某一范围的元素和,需要该范围左上角和右下角的元素下标,设ansi j对应mat中的范围左上角元素matx1 y1,右下角的元素matx2 y2。即
ans[i][j] = dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]
ansi j对应mati j向四周扩展,为了防止越界,matx1 y1即mat max(0, i-k) max(0, j-k) ,matx2 y2即mat min(m-1, i+k) min(n-1, j+k) ,对应到dp,由于dp下标从1开始,x1、y1、x2、y2都要 +1。

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)), ans(m, vector<int>(n));
for (int i = 1;i < m + 1;i++)
for (int j = 1;j < n + 1;j++)
dp[i][j] = dp[i][j - 1] + dp[i - 1][j] - dp[i - 1][j - 1] + mat[i - 1][j - 1];
for (int i = 0;i < m;i++)
{
for (int j = 0;j < n;j++)
{
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
x1 = max(0, i - k) + 1;
y1 = max(0, j - k) + 1;
x2 = min(m - 1, i + k) + 1;
y2 = min(n - 1, j + k) + 1;
ans[i][j] = dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1 - 1] + dp[x1 - 1][y1 - 1];
}
}
return ans;
}
};