算法篇----前缀和
1.什么是前缀和
前缀和就是快速求出数组中某一个连续区间的和
2.怎么做
一般我们是分为两步:
第一步:预处理出来一个前缀和数组,什么是前缀和数组呢?
前缀和数组就是一个数组,它里面存储的数据是原数组前n个数据的和,这里我们先借用一下动态递归里面的dp
我们规定,dp[i]表示:[1,i]区间内所有元素的和,举个例子,
倘若arr=[1,4,7,2,5,8,3,6,9]
那么dp=[1,5,12,14,19,27,30,36,45]
第二步:使用前缀和数组
3.经典例题
3.1【模板】前缀和
方法二)前缀和
我们还是想上文提到一样,求一下dp,之后题目让我们求区间[l,r]的前缀和,我们就dp[r]-dp[l-1]就好了,原理很简单不证明了。
这里还有个小细节:
下标为什么从1开始计数,原因就在于当我们想查询[0,2]时,那就变成dp[2]-dp[-1]了,那数组下标还有-1吗?肯定就越界了啊!倘若我们想查询[1,2]时,返回的是dp[2]-dp[0],而dp[0]=0,不影响结果!!!
说白了这么做就是为了处理边界情况,通过添加虚拟节点的方式
参考代码:
#include <iostream>
#include<vector>
using namespace std;int main()
{//读取数据int n,q;cin>>n>>q;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(q--){cin>>l>>r;cout<<dp[r]-dp[l-1]<<endl;}return 0;
}
3.2 【模板】⼆维前缀和
【模板】二维前缀和_牛客题霸_牛客网
这道题要求我们输出以 (x1, y1) 为左上角 , (x2,y2) 为右下角的子矩阵的和
解题思路:
方法一)暴力破解
我们就按照题目说的做就好,正面破解,但是时间复杂度是O(N*M*Q),毫无疑问,铁定超时!
方法二)前缀和
还是跟上一题差不多,这里要预处理出来一个前缀和矩阵,dp[i][j],
其中,dp[i][j]表示从[1,1]到[i.j]这段区间里面所有元素的和
那我们咋求呢?两层for?那搞不好又超时了,那很坏了
这里我们借助一些数学知识:假设要求dp[i][j],我们可以将其分成四块
这样以来,
原式=A+B+C+D
B和C不太好求,我们转化一下
原式=A+B + A+C +D -A
=dp[i-1][j] + dp[i][j-1] +arr [i][j] - dp[i-1][j-1]
好!基础措施做完了~
第二步应该就是使用前缀和矩阵了,题目让我们求[x1,y1]~[x2,y2]的
我们可以利用前面求出的矩阵,还是先画个图:
所以可以推导出解题公式了:
之后把公式往代码里面套就好啦~
参考代码:
#include <iostream>
#include <vector>
using namespace std;int main()
{//1.读入数据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];}}//2.预处理前缀和矩阵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];}}//3.使用前缀和矩阵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.3 寻找数组的中心下标
724. 寻找数组的中心下标 - 力扣(LeetCode)
解题思路:
方法一)暴力破解
就是从头到尾依次把arr[i]假设为中心,之后遍历看左右两侧是否满足要求,毫无疑问这种方法会超时,时间复杂度为O(N^2)
方法二)前缀和
题目要求的不还是让我们求前N个数的和吗?这样的话我们就可以使用前缀和的算法思想了,不同的是,这里我们要使用两次,如下图所示:
这里我们构造两个和数组,一个是f[i],另一个是g[i]
其中,f表示前缀和数组:f[i]表示区间[0,i-1],所有元素的和,状态方程为f[i]=f[i-1]+arr[i-1]
g表示后缀和数组:g[i]表示区间[i+1,n-1],所有元素的和,状态方程为g[i]=g[i+1]+arr[i+1]
构造完成后,我们只需要在区间[0,n-1],找到一个下标i,使得f[i]==g[i],即可解决
之后还有几个细节问题,f(0)=0,g(n-1)=0,f在填充时是从左向右的,g在填充时是从右向左的
参考代码:
class Solution {
public:int pivotIndex(vector<int>& nums) {int n=nums.size();vector<int> f(n),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;}
};
3.4 除自身以外数组的乘积
238. 除自身以外数组的乘积 - 力扣(LeetCode)
解题思路:
方法一)暴力破解
这种方法时间复杂度为O(N^2)肯定会超时,具体做法跟上面那个题差不多,不多说了!
方法二)前缀积
我们还是将数组分为两段,前面的为f,后面的为g,具体思路跟上一题差不多
我们写一下状态方程:
f[i]表示区间[0,n-1]的乘积,g[i]表示区间[i+1,n-n]的乘积
f[i]=f[i-1]*arr[i-1]
g[i]=g[i-1]*arr[i-1]
处理一下细节问题,f和g的第一个位置都不能为0,否则会导致我们的前缀积数组全是零,为此我们将f(0)=g(n-1)=1即可
本题的返回值就是f[i]*g[i]
参考代码:
class Solution {
public:vector<int> productExceptSelf(vector<int>& nums) {int n=nums.size();vector<int> f(n),g(n);f[0]=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];vector<int> ret(n);for(int i=0;i<n;i++){ret[i]=f[i]*g[i];}return ret;}
};
3.5 和为 k 的子数组
560. 和为 K 的子数组 - 力扣(LeetCode)
解题思路:前缀和+哈希表
不理解的看一下图:
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 x:nums){sum+=x; //计算当前位置的前缀和if(hash.count(sum-k)) ret+=hash[sum-k]; //统计个数hash[sum]++;}return ret;}
}
3.6 和可被 K 整除的子数组
974. 和可被 K 整除的子数组 - 力扣(LeetCode)
解题思路:
前置知识:
解题思路与上一题相似!
参考代码:
class Solution {
public:int subarraysDivByK(vector<int>& nums, int k) {unordered_map<int,int> hash;hash[0%k]=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;}
};
3.7 连续数组
525. 连续数组 - 力扣(LeetCode)
解题思路
这道题如果正面破解会很费力,但是倘若我们将0转换为-1,那么问题就会好解决很多,相当于我们直接找前缀和为0的最长子数组就好了!具体代码与前面的代码差不多~,我们还是借助哈希表来解决~hash<int,int>,第一个int 代表前缀和,第二个代表当前的下标
参考代码
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; //计算前缀和,顺便将0都置换成1if(hash.count(sum)) //count函数返回当前sum对应的键值对的那个下标,这里的哈希表是 hash<sum,index>{//说明这个前缀和之前就出现过·是存在的,那我们就要更新一下长度ret=max(ret,i-hash[sum]);}else{//说明这个前缀和之前没有出现过,那我们就把它放到哈希表中hash[sum]=i;}}return ret;}
};
3.8 矩阵区域和
1314. 矩阵区域和 - 力扣(LeetCode)
解题思路
这道题要求我们算出一个矩阵小单元一圈所有元素的和,那我们应该怎么算呢?
我们参考二维前缀和数组模板,先求一下递归公式:
假设我们要求[i][j]位置的一圈的元素和,不难推到出如下公式:
dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+mat[i][j]
之后便是使用了,同理写出公式:
但是那个x1,x2,y1,y2是什么呢?怎么确定呢?
回到题干,假设我们有任意一个位置坐标ans[i][j],我们如何确定x1,x2,y1,y2?由题意并且画个图,知道:
x1=i-k y1=j-k
x2=i+k y2=j+k
但是万一超出边界了呢?所以我们略改一下:
x1=max(0,i-k) y1=max(0,j-k)
x2=min(m-1,i+k) y2=min(n-1,j+k)
但是为了处理好ans(x,y)和dp(x+1,y+1)的关系,我们在刚刚求出的x1,x2,y1,y2都加上1即可!
参考代码:
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));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]; //这里的前缀和矩阵公式与刚刚讲的略有不同,这里的经过了优化(可以理解为它的下标是从 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,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;}
};
前缀和内容结束!