算法沉淀——前缀和
- 01.一维前缀和
- 02.二维前缀和
- 03.寻找数组的中心下标
- 04.除自身以外数组的乘积
- 05.和为 K 的子数组
- 06.和可被 K 整除的子数组
- 07.连续数组
- 08.矩阵区域和
前缀和算法是一种用于高效计算数组或序列中某个范围内元素之和的技巧。它通过预先计算数组的前缀和,并将这些前缀和保存在辅助数组中,从而在查询某个区间的和时能够以常数时间复杂度进行计算。
在实际应用中,前缀和算法经常用于解决与区间和相关的问题,例如子数组和的最大值、最小值、等于目标值的个数等。前缀和的应用能够优化问题的时间复杂度,提高算法的效率。
01.一维前缀和
题目链接:https://www.nowcoder.com/practice/acead2f4c28c401889915da98ecdc6bf?tpId=230&tqId=2021480&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D196
描述
给定一个长度为n的数组a1,a2,....an
.
接下来有q次查询, 每次查询有两个参数l, r
.
对于每个询问, 请输出al+al+1+....+ar
输入描述:
第一行包含两个整数n和q.
第二行包含n个整数, 表示a1,a2,....an
.
接下来q行,每行包含两个整数 l和r.
输出描述:
输出q行,每行代表一次查询的结果.
示例1
输入:
3 2
1 2 4
1 2
2 3
输出:
3
6
思路
- 通过数组
arr
存储输入的 n 个整数,数组dp
存储数组arr
的前缀和。 - 使用循环读取数组元素,并计算前缀和
dp
。 - 进行 q 次查询,每次查询给定一个区间 [l, r]。查询结果为
dp[r] - dp[l-1]
,表示数组在区间 [l, r] 的和。 - 输出每次查询的结果。
代码
#include <iostream>
using namespace std;
int main() {
int n,q;
cin>>n>>q;
long long arr[100001]={0},dp[100001]={0};
for(int i=1;i<=n;++i){
cin>>arr[i];
dp[i]=arr[i]+dp[i-1];
}
while(q--){
int l,r;
cin>>l>>r;
cout<<dp[r]-dp[l-1]<<endl;
}
return 0;
}
02.二维前缀和
题目链接:https://www.nowcoder.com/practice/99eb8040d116414ea3296467ce81cbbc?tpId=230&tqId=2023819&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=%2Fexam%2Foj%3Fpage%3D1%26tab%3D%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587%26topicId%3D196
描述
给你一个 n 行 m 列的矩阵 A ,下标从1开始。
接下来有 q 次查询,每次查询输入 4 个参数 x1 , y1 , x2 , y2
请输出以 (x1, y1)
为左上角 , (x2,y2)
为右下角的子矩阵的和,
输入描述:
第一行包含三个整数n,m,q.
接下来n行,每行m个整数,代表矩阵的元素
接下来q行,每行4个整数x1, y1, x2, y2
,分别代表这次查询的参数
1≤n,m≤1000
1≤q≤10^5
−109≤`a[i][j]`≤109
11≤x1≤x2≤n
1≤y1≤y2≤m
输出描述:
输出q行,每行表示查询结果。
示例1
输入:
3 4 3
1 2 3 4
3 2 1 0
1 5 7 8
1 1 2 2
1 1 3 3
1 2 3 4
输出:
8
25
32
思路
首先要注意下面求区域和的坐标是相对位置坐标,而不是数组下标,根据题意我们在前缀和数组中存储时,每个位置存储的都应该是相对起始位置的矩阵和,这样我们可以通过计算得到某一个区域内的矩阵和
前缀和计算
使用前缀和计算矩阵和
通过这两个公式我们就可以来进行代码的编写了
- 通过数组
arr
存储输入的二维数组元素,数组dp
存储二维数组的前缀和。 - 使用两层循环读取二维数组元素,并计算前缀和
dp
。 - 进行 q 次查询,每次查询给定一个矩形区域 [x1, y1, x2, y2]。查询结果为
dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]
,表示矩形区域的和。 - 输出每次查询的结果。
代码
#include <iostream>
using namespace std;
int arr[1100][1100];
long long dp[1100][1100];
int n,m,q,x1,x2,y1,y2;
int main() {
cin>>n>>m>>q;
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
cin>>arr[i][j];
dp[i][j]=dp[i-1][j]+dp[i][j-1]+arr[i][j]-dp[i-1][j-1];
}
}
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;
}
03.寻找数组的中心下标
题目链接:https://leetcode.cn/problems/find-pivot-index/
给你一个整数数组 nums
,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0
,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1
。
示例 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 ,二者相等。
示例 2:
输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。
示例 3:
输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。
思路
这里我们可以使用前缀和和后缀和两个数组,再进行遍历,相遇时相等的下标就是中心下标
- 创建两个辅助数组
l
和r
,分别表示数组nums
中每个位置左侧元素的和和右侧元素的和。 - 通过两个循环分别计算
l
和r
数组的值。在计算l
数组时,从左到右累加nums
数组中当前位置及其左侧的元素。在计算r
数组时,从右到左累加nums
数组中当前位置及其右侧的元素。 - 最后,再通过一个循环遍历数组
nums
,找到一个索引,使得l[i]
(左侧元素的和)等于r[i]
(右侧元素的和),如果找到了这样的索引,则返回该索引;否则返回 -1。
代码
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n=nums.size();
vector<int> l(n),r(n);
for(int i=1,j=n-2;i<n;i++,j--){
l[i]=l[i-1]+nums[i-1];
r[j]=r[j+1]+nums[j+1];
}
for(int i=0;i<n;i++)
if(l[i]==r[i]) return i;
return -1;
}
};
04.除自身以外数组的乘积
题目链接:https://leetcode.cn/problems/product-of-array-except-self/
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 **不要使用除法,**且在 O(*n*)
时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
提示:
2 <= nums.length <= 105
-30 <= nums[i] <= 30
- 保证 数组
nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内
思路
首先我们这里可以使用前缀和思想,类似上一题,我们求出前缀积和后缀积,但在这里我们要错开一位,错开的那一位补1,不影响最后的运算结果,之后错位相乘就是剩下的数之积
- 创建两个辅助数组
lm
和rm
,分别表示数组nums
中每个位置左侧元素的乘积和右侧元素的乘积。这两个数组的长度都为n + 1
,并初始化为全 1。 - 通过两个循环分别计算
lm
和rm
数组的值。在计算lm
数组时,从左到右累乘nums
数组中当前位置及其左侧的元素。在计算rm
数组时,从右到左累乘nums
数组中当前位置及其右侧的元素。 - 最后,再通过一个循环遍历数组
nums
,计算ret[i]
的值,即lm[i]
与rm[i]
的乘积。
代码
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n=nums.size();
vector<int> lm(n+1,1),rm(n+1,1),ret(n);
for(int i=1,j=n-2;i<n;i++,j--){
lm[i]=lm[i-1]*nums[i-1];
rm[j]=rm[j+1]*nums[j+1];
}
for(int i=0;i<n;++i) ret[i]=lm[i]*rm[i];
return ret;
}
};
05.和为 K 的子数组
题目链接:https://leetcode.cn/problems/subarray-sum-equals-k/
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
思路
这里我们使用前缀和算法时,不用真的去创建前缀和数组,而是里哈希辅助记录前缀和次数标记,计算sum-k如果在哈希中有标记,说明前面累加时存在和合法子数组,累加次数。
- 初始化一个哈希表
hash
,其中hash[0]
表示和为0
的子数组的个数,初始化为1
。 - 使用两个变量
sum
和ret
,其中sum
表示当前累积的和,ret
表示满足条件的子数组的个数,初始化为0
。 - 遍历数组
nums
,累积和并更新哈希表。- 对于每个元素
x
,更新sum += x
。 - 检查是否存在之前的累积和
sum - k
在哈希表中,如果存在,则累加hash[sum - k]
到ret
。 - 更新哈希表中的当前累积和
sum
的计数。
- 对于每个元素
- 返回最终的结果
ret
。
举例说明:
考虑输入数组 nums = [1, 2, 3, 4, 5]
,以及 k = 7
。
- 初始时,
sum = 0
,hash[0] = 1
。 - 遍历到元素
1
时,sum = 1
,检查hash[1 - 7]
(即hash[-6]
),不存在,继续。 - 遍历到元素
2
时,sum = 3
,检查hash[3 - 7]
(即hash[-4]
),不存在,继续。 - 遍历到元素
3
时,sum = 6
,检查hash[6 - 7]
(即hash[-1]
),不存在,继续。 - 遍历到元素
4
时,sum = 10
,检查hash[10 - 7]
(即hash[3]
),存在,累加hash[3]
(即ret += 1
)。 - 遍历到元素
5
时,sum = 15
,检查hash[15 - 7]
(即hash[8]
),不存在,继续。
最终,ret
的值为 1
,表示有一个和为 7
的子数组。
代码
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> hash;
hash[0]=1;
int sum=0,ret=0;
for(const auto& x : nums){
sum+=x;
if(hash.count(sum-k)) ret+=hash[sum-k];
hash[sum]++;
}
return ret;
}
};
06.和可被 K 整除的子数组
题目链接:https://leetcode.cn/problems/subarray-sums-divisible-by-k/
给定一个整数数组 nums
和一个整数 k
,返回其中元素之和可被 k
整除的(连续、非空) 子数组 的数目。
子数组 是数组的 连续 部分。
示例 1:
输入: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]
示例 2:
输入: nums = [5], k = 9
输出: 0
提示:
1 <= nums.length <= 3 * 104
-104 <= nums[i] <= 104
2 <= k <= 104
思路
其实这里和上面的解法是一致的,只不过我们这里需要注意一点就是关于负数取余的修正。
- 初始化哈希表:
unordered_map<int, int> hash;
表示创建一个哈希表,用于存储前缀和的余数以及对应的出现次数。其中,hash[0 % k] = 1;
表示初始时,前缀和为 0 的余数的个数为 1。 - 遍历数组: 在
for(auto x : nums)
的循环中,遍历数组nums
。对于每个元素x
,执行以下操作:sum += x;
累加当前位置的元素,得到当前位置的前缀和。int r = (sum % k + k) % k;
计算修正后的余数r
,确保余数为正。这一步的目的是处理负数的情况,保证余数范围在 [0, k-1] 之间。if(hash.count(r)) ret += hash[r];
检查之前是否存在相同的余数r
,如果存在,则将哈希表中对应的次数累加到结果ret
中。hash[r]++;
更新哈希表,将当前余数r
的次数加一。
- 返回结果: 最终返回
ret
,即和可被 K 整除的子数组个数。
核心思想是利用哈希表存储前缀和的余数以及对应的出现次数,通过统计相同余数出现的次数,得到满足条件的子数组个数。
举例说明:
考虑输入数组 nums = [4,5,0,-2,-3,1]
和 k = 5
。
- 初始时,
sum = 0
,hash[0 % 5] = 1
。 - 遍历到元素
4
时,sum = 4
,r = (4 % 5 + 5) % 5 = 4
,检查哈希表中是否有余数为4
的记录,不存在,继续。 - 遍历到元素
5
时,sum = 9
,r = (9 % 5 + 5) % 5 = 4
,检查哈希表中是否有余数为4
的记录,存在,累加哈希表中的次数(即ret += 1
),更新哈希表。 - 以此类推,最终得到和可被 K 整除的子数组个数。
代码
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int,int> hash;
hash[0%k]=1;
int sum=0,ret=0;
for(const auto& x:nums){
sum+=x;
int r=(sum%k+k)%k;
if(hash.count(r)) ret+=hash[r];
hash[r]++;
}
return ret;
}
};
07.连续数组
题目链接:https://leetcode.cn/problems/contiguous-array/
给定一个二进制数组 nums
, 找到含有相同数量的 0
和 1
的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:
输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
提示:
1 <= nums.length <= 105
nums[i]
不是0
就是1
思路
这里我们将问题转化成和为0的最大长度即可,将0当作-1,即与前几题解法类似
- 初始化哈希表:
unordered_map<int, int> hash;
表示创建一个哈希表,用于存储前缀和以及对应的出现位置。初始时,将前缀和为 0 的位置设为 -1,以处理从数组开头开始的子数组。 - 遍历数组: 在
for(int i=0; i<nums.size(); ++i)
的循环中,遍历数组nums
。对于每个元素nums[i]
,执行以下操作:- 如果
nums[i]
是 0,则令sum += -1
;如果是 1,则令sum += 1
。这样,sum
就表示当前位置的前缀和,其中 0 对应 -1,1 对应 1。 - 判断哈希表中是否存在当前前缀和
sum
。如果存在,说明从上次该前缀和出现的位置到当前位置的子数组的和为零,更新最长长度ret
。 - 如果哈希表中不存在当前前缀和
sum
,则将当前前缀和和对应的位置存入哈希表中。
- 如果
- 返回结果: 最终返回
ret
,即连续子数组的和为零的最长长度。
这种方法的核心思想是通过维护一个哈希表,记录每个前缀和第一次出现的位置。当相同的前缀和再次出现时,说明两次出现之间的子数组的和为零,从而可以更新最长长度。
举例说明:
考虑输入数组 nums = [0,1,0,1]
。
- 初始时,
sum = 0
,hash[0] = -1
。 - 遍历到第一个元素
0
时,sum = -1
,哈希表中没有-1
,将当前前缀和和对应的位置存入哈希表中,得到hash[-1] = 0
。 - 遍历到第二个元素
1
时,sum = 0
,哈希表中存在0
,更新最长长度ret = 2
。 - 以此类推,最终得到连续子数组的和为零的最长长度。
代码
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;
}
};
08.矩阵区域和
题目链接:https://leetcode.cn/problems/matrix-block-sum/
给你一个 m x n
的矩阵 mat
和一个整数 k
,请你返回一个矩阵 answer
,其中每个 answer[i][j]
是所有满足下述条件的元素 mat[r][c]
的和:
i - k <= r <= i + k,
j - k <= c <= j + k
且(r, c)
在矩阵内。
示例 1:
输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1
输出:[[12,21,16],[27,45,33],[24,39,28]]
示例 2:
输入: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
思路
这里其实是一个经典的二维前缀和问题,但是需要处理好边界问题和相对位置,计算公式可参考本篇文章第二道算法题模板。
- 初始化:
int m=mat.size(),n=mat[0].size();
获取输入矩阵的行数和列数。vector<vector<int>> dp(m+1,vector<int>(n+1));
创建二维数组dp
,用于存储矩阵的前缀和。
- 计算前缀和数组
dp
:- 使用两层循环遍历矩阵中的每个元素,计算前缀和数组
dp
。 dp[i][j]
表示从矩阵左上角(0,0)
到(i-1,j-1)
的元素和。
- 使用两层循环遍历矩阵中的每个元素,计算前缀和数组
- 计算每个块的和:
- 使用两层嵌套循环遍历矩阵中的每个元素,计算每个块的和。
- 对于每个位置
(i,j)
,计算块的左上角和右下角的坐标,确保不超过矩阵边界。 - 利用前缀和数组
dp
计算块的和,并将结果存入结果矩阵ret[i][j]
中。
- 返回结果矩阵
ret
: 包含了每个块的和。
代码
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));
vector<vector<int>> ret(m,vector<int>(n));
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];
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;
ret[i][j]=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1];
}
}
return ret;
}
};