一、【模版】前缀和
1.链接
【模板】前缀和_牛客题霸_牛客网 (nowcoder.com)
2.描述
3.思路
前缀和的思想其实就是一种简单的动态规划,以i位置记录从头位置到i位置的和,然后间接的求一段连续区间的数组和,时间复杂度是O(n) + O(q),这种思想在实际中是为了应对多次查询的情况,当q特别大时,采用这种方式的时间复杂度就会较低
4.参考代码
#include <iostream>
#include <vector>
using namespace std;int main()
{int n,q;cin >> n >> q;vector<int> arr(n+1,0);for(int i = 1;i <= n;i++) cin >> arr[i];vector<long long> dp(n+1,0);for(int i = 1;i <= n ; i++) dp[i] = dp[i-1] + arr[i];while(q--){int l,r;cin >> l >> r;cout << dp[r] - dp[l-1] << endl;}return 0;
}
二、二维前缀和
1.链接
【模板】二维前缀和_牛客题霸_牛客网 (nowcoder.com)
2.描述
3.思路
该题我们采用动态规划的定式思路去分析得到前缀和的表,并且分析如何使用
4.参考代码
核心的思路就是上面的动归分析已经如何使用前缀和表格的部分,剩下的就是在实际写代码时候的一些细节
1.测试用例中包含较大的数据,因此在dp表格中存放的值类型需要使用long long
2.一般按照思路是先载入数据,然后再动归建立dp表,但由于我们这里dp表和arr都选择使用多一行一列的辅助位置去进行的初始化,这两个步骤可以放在同一个for循环里一起执行,但先后顺序不能变,一定是先载入arr的数据,再去执行dp
#include <iostream>
using namespace std;
#include<vector>int main()
{//加载数据int n,m,q;cin >> n >> m >> q;vector<vector<int>> arr(n+1,vector<int>(m+1,0));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++){cin >> arr[i][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[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1] << endl;}return 0;
}
三、寻找数组的中心下标
1.链接
724. 寻找数组的中心下标 - 力扣(LeetCode)
2.描述
3.思路
先建立前缀和表格,然后遍历一遍下标位置,去比对当前下标的前后两个部分的和是否相同,若是相同则说明当前下标就是目标值,直接返回,若是遍历结束后都没有找到,说明不存在,返回-1
要注意前缀表中和题目给的数组两者之间的映射关系,最好画图去分析,又或者可以建立多一个后缀表,去对应遍历
4.参考代码
class Solution {
public:int pivotIndex(vector<int>& nums) {int n = nums.size();vector<int> dp(n+1,0);for(int i = 1;i<=n;i++) dp[i] = dp[i-1] + nums[i-1];for(int i = 0;i<n;i++){if(dp[i] == dp[n] - dp[i+1]) return i;}return -1;}
};
四、除自身以外数组的乘积
1.链接
238. 除自身以外数组的乘积 - 力扣(LeetCode)
2.描述
3.思路
题目要求的数组是除开自己的其余所有数的乘积,那么可以将ret[i]分成两部分
1.nums[0] * nums[1] * nums[2] * ... * nums[i-1] (i位置的左半部分乘积)
2.nums[i+1] * nums[i+2] * nums[i+3] * ... * nums[n-1](i位置的右半部分乘积)
因此我们可以利用前缀和的思想,去将前半部分和后半部分分别进行制表
head[i]:表示以i位置结束,从第头到该位置的乘积
tail[i]:表示从i位置开始,到最末尾的乘积
然后遍历填表即可
4.参考代码
class Solution {
public:vector<int> productExceptSelf(vector<int>& nums) {int n = nums.size();vector<int> ret(n);vector<int> head(n+1,1);//添加辅助位要注意映射关系vector<int> tail(n+2,1);//注意,这里需要先初始化tail的第一个值for(int i = 1;i<=n;i++) head[i] = head[i-1]*nums[i-1];for(int i = n;i>=1;i--) tail[i] = tail[i+1]*nums[i-1];//得到两个表格后,遍历填表即可for(int i = 0;i<n;i++) ret[i] = head[i]*tail[i+2];return ret;}
};
五、和为k的子数组
1.链接
560. 和为 K 的子数组 - 力扣(LeetCode)
2.描述
3.思路
4.参考代码
class Solution {
public:int subarraySum(vector<int>& nums, int k) {int sum = 0;map<int,int> hash;int count = 0;for(int i = 0;i<nums.size();i++){hash[sum]++;sum += nums[i];//此时sum为i位置的前缀和count += hash[sum-k];}return count;}
};
5.代码分析
六、和可被K整除的子数组
1.链接
974. 和可被 K 整除的子数组 - 力扣(LeetCode)
2.描述
3.思路
4.参考代码
class Solution
{
public:int subarraysDivByK(vector<int>& nums, int k) {map<int,int> hash;int sum = nums[0];int count = 0;for(int i = 0;i<nums.size();i++){hash[(sum%k+k)%k]++;sum+=nums[i];count += hash[(sum%k+k)%k];}return count;}
};
七、连续数组
1.链接
525. 连续数组 - 力扣(LeetCode)
2.描述
3.思路
改题目若是将所有的0都换成-1,则依然和上一题的思路是一样的,都是通过前缀和去转化
4.参考代码
class Solution
{
public:int findMaxLength(vector<int>& nums) {unordered_map<int,int> hash;int ret = 0;int sum = 0;hash[0] = -1;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;}
};
八、矩阵区域和
1.链接
1314. 矩阵区域和 - 力扣(LeetCode)
2.描述
这题描述较复杂,大致意思就是,给你一个m*n的矩阵,并且给你一整数k,然后你得返回一个相同规模的矩阵,而这个矩阵内的数据要求是:
以【i,j】位置向上下左右各延伸k个单位然后围成的矩阵和,越界的部分视为0,例如:
3.思路
这题就是对二维前缀和的一个应用,对二维前缀和的表格建立和使用,需要熟练掌握分析,而不要去死记公式,得到二维前缀和表和得到使用方法后再根据这题进行分析,这里不重复分析,随便花个草图去分析即可
建表的递推公式:dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1] + mat[i][j]
使用表格的公式:dp[x2][y2] - dp[x1-1][y2] - dp[x2][y1-1] + dp[x1-1][y1-1]
分析:
这题首先是如何确定x1、y1、x2、y2的问题,画图分析可得(太简单了略)
(x1,y1) = (i-k,y-k) , (x2,y2) = (i+k,y+k)
除了找到对应的矩阵区间,我们很容易想到还需要对边界条件进行除了,i-k和j-k是有可能越界的,因此最多我们不能让它们小于(0,0)的位置,i+k和j+k同理,最大不能大于(m-1,n-1)的位置
x1 = max(0,i-k); y1 = max(0,y-k); x2 = min(m-1,i+k); y2 = min(n-1,j+k);
还有一个细节就是下标的映射关系要注意,因为在初始化dp表(前缀和表)时,我们会添加一个辅助位,而题目给的数组是从0开始的,dp表则是从1开始,所以写代码时要注意映射关系即可
4.参考代码
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,0));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>> 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), y1=max(0,j-k);int x2 = min(m-1,i+k), y2 = min(n-1,j+k);answer[i][j] = dp[x2+1][y2+1] - dp[x1][y2+1] - dp[x2+1][y1] + dp[x1][y1];//注意映射关系}}return answer;}
};
总结
本篇内容是关于前缀和的算法思想和应用,整理了一些经典的题目,从简单到难逐步递进,提供链接可以直接到力扣上做,也提供了描述可以直接通过看本篇文章去尝试思考解题,提供了参考思路和测试通过的代码(C++),整理学习下来后,个人认为一个是需要掌握一维和二维的表格建立和基本使用,还有相对较难的,但也有迹可循的一种用前缀和将题目转化,利用哈希表去优化效率的思想,参考五到七题,这个思路相对重要