优选算法《前缀和》

优选算法《前缀和》

在之前的篇章当中我们已经了解了双指针、滑动窗口、二分查找算法,那么接下来在本篇当中我们将继续进行算法的学习,在本篇当中我们学习的算法是前缀和算法。在此会先了解前缀和算法是什么,之后再了解前缀和算法的适用场景,再依次了解一维前缀和和二维前缀和,最后再了解完算法原理之后,还是和之前一样通过题目解析、算法原理讲解、代码实现的三步来完成代码习题。一起加油吧!!


1.前缀和算法

在此要了解什么是前缀和就来通过以下的算法题来了解

 1.1 一维前缀和 

【模板】前缀和_牛客题霸_牛客网

题目解析 

通过以上的题目描述就可以看出以上算法题要我们实现的是创建一个大小为n+1的数组,在此将数组大小创建为n+1是由于数组当中的元素是从下标1开始计数的。之后要进行的操作就是将数组当中指定下标区间内的元素之和。

键盘在输入的第一行第一个数值n+1就表示数组的大小,之后第一行的第二个数值q就表示要进行查找的次数。第二行的就表示要插入到数组当中的元素,之后每行的元素就表示表示要查询区间的边界。

例如以上的示例1,第一行输入为3 2,就表示数组的大小要为4,之后会进行三次的查询

算法原理讲解 

在了解了题目的要求之后接下来就来思考如何解决该算法题

首先能想到的解法就是使用暴力枚举,也就是每次都直接从数组当中的下标l开始,一直访问到r下标,在此 过程中创建一个变量sum来统计数组对应区间的所有元素值之和。但是这种算法在给定的区间为[1,n] 时就需要遍历数组一次,这也就使得时间复杂度为 O(n) 。再结合要进行q次的查询,那么总的时间复杂度就为O(n*q),在这道题的数据范围下肯定是会超时的。

此时我们就要来思考效率更好的算法了

在此更优秀的解法就是接下来要讲的前缀和算法了,前缀和算法能实现的是快速的求出数组某一个区间的和,这里的快速指的就是使用O(1)的时间复杂度实现查找。

前缀和数组要分为以下的几步:

1.预处理一个前缀和数组

2.使用前缀和数组

在此首先要创建一个前缀和数组,大小为n+1,将该数组命名为dp。dp数组中的每个下标为i的元素内的元素值为 [ 1 , i ]区间内数组a所有元素的和

例如以下示例:

那么通过以上示例就可以得出dp数组内的元素值 dp[i]=dp[i-1] + a[i],那么在此也就能理解了为什么我们的前缀和数组dp也要在初始化时空间为n+1,这是因为当i=1时,dp[i] =dp[0] + a[0],此时就不会出现数组越界的问题。而如果dp数组的大小为n,起始的数组元素从0开始使用就会使得i=0时,出现dp[0] =dp[-1] +dp[0],此时的dp[-1]就会造成程序的奔溃

以上预处理出了前缀和数组之红接下来就是使用前缀和数组

在此在该题当中要我们求的是原数组区间 [ l , r ] 之间的元素值之和,那么此时就可以直接使用我们创建的前缀和数组,元素之和就为 dp[r] - dp[l-1] 。在此数组下标从1开始计数也使得不会出现越界访问的问题,而如果数组下标从0开始计数就会出现 当区间为 [0,2] 这种情况时,会出现dp[2] - dp[-1]此时就会造成程序的奔溃

因此总的来说下标从1开始计数的目的是为了处理边界情况。

代码实现

#include <iostream> #include<vector> using namespace std; int main() { //n表示数组内有效的元素个数,q表示要进行查询的次数 int n,q; //读取用户输入的n和q cin>>n>>q; //创建一个大小为n的数组并且一开始将数组的元素都初始化为0 vector<int> a(n+1,0); //读取用户输入的数组元素 for(int i=1;i<=n;i++) { cin>>a[i]; } //创建一个前缀和数组,大小为n+1,数据范围可能超出int最大值,因此dp数组类型使用long long vector<long long> dp(n+1); //预处理出前缀和数组 for(int i=1;i<=n;i++) { dp[i]=dp[i-1]+a[i]; } //区间[l,r] int r,l; //进行q次的查询 while(q--) { //读取用户输入的l,r cin>>l>>r; cout<<dp[r]-dp[l-1]<<endl; } } 

注:由于本题的a数组内的元素最大值为10^9,因此为了避免元素值的越界,因此将dp数组类型定义为vector<long long> 

 

 

1.2 二维前缀和

在以上我们了解了一维的前缀和,那么接下来继续来学习二维前缀和,在此还是通过一道算法题来了解

【模板】二维前缀和_牛客题霸_牛客网

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是通过题目给定的n行m列的矩阵进行q次的查询,每次输出要求的以(x1,y1)为左上角,(x2,y2)为右下角的矩阵元素值之和

例如以上的示例1:

算法原理讲解 

在了解了题目的要求之后接下来就来思考如何解决该算法题

首先能想到的解法就是使用暴力枚举,也就是每次都直接从矩阵的(x1,y1)开始,一直访问到(x2,y2)为止,在此 过程中创建一个变量sum来统计数组对应子矩阵的所有元素值之和。但是这种算法在给定的子矩阵为(1,1)  ,(n,m) 时就需要遍历原矩阵一次,这也就使得时间复杂度为 O(n^2) 。再结合要进行q次的查询,那么总的时间复杂度就为O((n^2)*q),在这道题的数据范围下肯定是会超时的。

那么这时我们就要想如何进行算法的优化 ,在此其实还是使用前缀和的算法,不同于之前的是,在此要在创建前缀和数组是二维数组。

接下来就来推导二维前缀和数组的递推公式是什么样的,在此就可以通过画图的方式来推导

在以上图示当中(i,j)位置就表示要计算的前缀和的下标,在此该位置的前缀和就是从下标(1,1)到(i,j)位置的全部元素之和。 那么以上图示的就是要求出区域A,B,C,D的所有元素之和。

但是此时问题就来了,这其中的B,C区域的元素之和是不好算的,那么有什么办法进行问题的转化呢?

其实在此就可以看出(A+B)和(A+C)区域的元素是较为好算的去,(A+B)区域的元素之和就是下标(i-1,j)位置的前缀和,(A+C)区域的元素之和就是下标(i,j-1)位置的前缀和。D区域就好算了就是二维数组下标(i,j)位置的元素值。

注:在此一开始创建的二维数组第0行和第0列的不进行元素的存储,在此也是为了避免在特定情况下出现越界。

在此设前缀和数组为dp,二维数组为a

那么综上就可以得出下标为(i,j)位置的前缀和公式为:
 (A+B)+(A+C)+D-A=dp[i-1][j]+dp[i][j-1]+a[i][j]-dp[i-1][j-1]。

预处理出来了前缀和数组之后接下来就来思考如何使用前缀和数组

因为在该算法题当中我们要求的是(x1,y1)到(x2,y2)子矩阵区间内的全部元素之和。那么接下来还是画图分析

如上所示我们就是要求出区间D内的全部元素之和,但是在此D区域元素之和不那么好求,在此就要和之前求前缀和一样试着转换思路;既然直接求不好求,那么就可以先求出整个(A+B+C+D)区域的前缀和,之后再将这个和减去(A+B)和(A+C)区域的前缀和,最后再加上A区域的前缀和,这样就能实现求出D区域所有元素的和。

 因此综上所述递推公式就为:
(A+B+C+D)-(A+B)-(A+C)+A=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1]

代码实现

#include <iostream> #include<vector> using namespace std; int main() { //创建变量n,m,q分别表示矩阵的有效行数、列数以及要进行查询的次数 int n,m,q; cin>>n>>m>>q; //创建大小为n+1的数组,该数组内每个元素为大小为m+1的数组 vector<vector<int>> a(n+1,vector<int>(m+1)); //将用户输入的元素输入到二维数组a内 for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { cin>>a[i][j]; } } //创建前缀和数组dp 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]+a[i][j]-dp[i-1][j-1]; } } //进行q次查询 while(q--) { //读取用户输入的下标 int x1,y1,x2,y2; cin>>x1>>y1>>x2>>y2; cout<<dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1]<<endl; } } 

注:以上在创建前缀和数组时元素值可能超出int范围, 因此前缀和数组类型使用long long

 

2. 前缀和算法练习题

在以上我们了解了一维前缀和以及二维前缀和的模板,接下来我们就通过几道算法题来巩固。注意以上学习的模板切记不要死记硬背,要灵活运用,解题过程中结合画图就很简单。

2.1 寻找数组的中心下标

724. 寻找数组的中心下标 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是找出满足数组下标两侧和相等的了数组下标,如果不存在满足条件的下标就返回-1。并且如果中心下标位于数组最左端,那么左侧数之和视为 0,在最右侧也如此。

例如以上题目的示例:
示例1当中在数组下标3的两侧元素值之和为11,此时该下标就为中心下标



示例2当中找不到满足条件的数组下标

 

示例3当中数组下标为0时,右侧和为0,左侧和为0,此时就为中心下标



算法原理讲解

在此最容易想到的解法就是通过遍历一次数组,之后再在每遍历一个数组的元素之后在创建两个变量Lsum和Rsum,从该数组元素开始往左统计左边的元素之和Lsum,往右边统计右边的元素之和Rsum。统计了之和比较Lsum和Rsum的值是否相等,若相等就说明该位置是中心下标,若不是就继续遍历数组直到数组的最后一个元素为止。如果到最后一个元素结束都没有一个数组下标为中心下标就返回-1。

但是以上这种暴力枚举的方式最坏的情况下,需要遍历数组一次,在此每个元素都要遍历数组一次。这样总的时间复杂度就为O(n^2),这样的效率是无法满足我们的要求的,因此要想出效率更高的算法。

其实在此该算法题就很适合使用前缀和来解决,但在此的前缀和和之前我们学习的不太相识,在此要得到的前缀和数组每个元素的值是不包括对应原数组下标的元素值的,这样就是为了让满足题目要求。

但是一个前缀和数组还无法满足我们的要求还需要得到数组元素之后的元素之和,那么此时就要在创建一个后缀和数组。

有了后最和数组就可以比较每个数组下标和前缀和数组对应位置的值是否相等,相等的话就说明该位置是中心下标。

完成了以上的算法分析接下来就来要思考前缀和和后缀和数组的递推公式是什么

在此原数组为nums,设前缀和数组为dp1,后缀和数组为dp2,
那么dp1[i]=dp[i-1]+nums[i-1];dp2[i]=dp[i+1]+nums[i+1]

以上我们就将两个数组的递推公式都完成了,但在此我们还要来处理一下边界的情况就是当dp1[0]和dp2[n-1]因为在这两个情况下都会出现数组越界的情况,因此要解决就需要在使用公式之前就将这两个元素初始化为0。

代码实现

class Solution { public: int pivotIndex(vector<int>& nums) { int n=nums.size(); //创建前缀和数组dp1和后缀和数组dp2 vector<int> dp1(n),dp2(n); //处理边界情况 dp1[0]=dp2[n-1]=0; //得到前缀和数组 for(int i=1;i<n;i++) { dp1[i]=dp1[i-1]+nums[i-1]; } //得到后缀和数组 for(int i=n-2;i>=0;i--) { dp2[i]=dp2[i+1]+nums[i+1]; } //判断是否有满足条件的中心下标 for(int i=0;i<n;i++) { if(dp1[i]==dp2[i])return i; } //没有满足条件的中心下标 return -1; } };

2.2 除自身以外数组的乘积

238. 除自身以外数组的乘积 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是返回给定的数组每个元素除了之身以外的元素值得乘积。并且在实现过程当中还不能使用到除法。

例如以上示例:


 

算法原理讲解 

在此在这道算法题当中最容易想到得算法就是使用暴力枚举得方式来解决。具体得过程就是先遍历一次数组使用变量sum来统计数组所有元素积。之后创建一个大小为n和原数组一样大的数组ret数组,之后该数组内的每个元素值为sum除去对应下标原数组内的值。

以上算法确实是能解决,但是问题是这道算法题要求我们不能使用除法,因此就需要想其他的算法了

其实在此就可以用到前缀和的思想,只不过在这道题当中和之前使用的前缀和不太相同的是在此要创建的是前缀积数组;并且还要创建两个数组分别是前缀积和后缀积,之后每个ret数组下标的元素的值就是对应位置前缀积元素的值乘后缀元素的值。在此这两个数组的大小就需要创建为n+1大小

那么接下来就需要来实现的是前缀积和后缀积数组的递推公式:

前缀积数组:F[i]=F[i-1]*nums[i-1]

后缀积数组:B[i]=B[i+1]*nums[i+1]

注:在此和之前寻找中心下标的那道算法题一样,也要处理数组的边界情况,在以上实现的两个数组边界情况分别是F[0]和B[n-1]。但在此和之前那道算法题不同的是因为存储的乘积此时就需要将这两个元素初始化为1。

代码实现

class Solution { public: vector<int> productExceptSelf(vector<int>& nums) { int n=nums.size(); //创建前缀积和后缀积数组 vector<int> F(n),B(n); //处理数组的边界情况 F[0]=B[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--) { B[i]=B[i+1]*nums[i+1]; } //返回的数组 vector<int> ret(n); for(int i=0;i<n;i++) { ret[i]=F[i]*B[i]; } return ret; } };


 

2.3 和为 k 的子数组

560. 和为 K 的子数组 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是统计给定的数组当中和为k的子数组的个数。

例如以上的示例:

算法原理讲解 

在这道算法题当中使用暴力枚举就是直接遍历一次数组之后从每个元素再向后统计区间的元素和,当区间的和为k时就将统计个数的变量count加一,这样遍历完数组就可以得到和为k的子数组的个数。

但是这种暴力枚举的时间复杂度为O(n^2),这样的效率是不优秀的,此时我们就要思考如何实现更优秀的算法。

在此看到要求子数组的问题你可能就会想使用滑动窗口算法来解决,确实这和我们之前使用滑动窗口的问题很相识,但是要知道在使用滑动窗口的前提是同向双指针也就是在数组当中定义了指针之后不能出现回退的情况,但是在这道算法题当中有一个细节要注意就是数组内的元素值可能为负数

 那么此时使用双指针时就可能出现回退的情况

因此在此就不能使用双指针算法来解决。那么有什么其他的方法呢?

其实在此还是可以使用前缀和的思想来解决,那么接下来就来分析实现的过程 

要求得到和为k的子数组,那么就来分析子数组的特性

在此当子数组和为k时,此时若到数组末尾元素的前缀和为sum,那么该子数组之前的前缀和就要为sum-k,那么就可以根据这个特性来解决这道题。

那么在每次遍历到数组的元素时就需要得到对应的前缀和为sum-k的个数,在此就可以使用一个哈希表来实现。哈希表内存储的就是对应的前缀和的个数

以上我们就大致的将算法的流程阐述完了,但还有几个要注意的点

首先就是每次将对应的前缀和存入到哈希表中是要在进行和为sum-k的前缀和查询之后进行。
其次就是若sum=k时那么这时的前缀和就为0,因此就需要在进行之后的操作时先将哈希表中值为0的映射值修改为1

代码实现

class Solution { public: int subarraySum(vector<int>& nums, int k) { //创建存储前缀和的哈希表 unordered_map<int,int> hash; //sum为遍历到位置的前缀和,count为和为为k的子数组的个数 int sum=0,count=0; //将哈希表中和为0的值初始化为1 hash[0]=1; //遍历原数组 for(auto x:nums) { sum+=x; //将值为前缀和为sum-k个数加到count当中 if(hash.count(sum-k))count+=hash[sum-k]; //将sum加入到哈希表当中 hash[sum]++; } return count; } };

 

2.4 和可被 K 整除的子数组

974. 和可被 K 整除的子数组 - 力扣(LeetCode)

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是在给定数组当中找出能被k整除的子数组的个数

例如以上的示例

能被5整除的有以下的子数组

算法原理讲解 

在这道题当中最容易想到的算法就是使用暴力枚举来实现,在此由于过程太简单就不再进行讲解,直接来思考如何进行算法的优化。

再进行算法的优化之前先来了解两个知识点

1.同余定理

(a-b)%p=k……0,则可以推导出a%b=b%p

注:以上同余定理在此不进行证明

2.在C++当中【负数%正数】的结果修正

在之前在C语言阶段的学习我们就知道%运算的结果是由第一个操作数的正负决定的,这时如果第一个操作数是负数时,当出现余数时就会为负数,在此要将余数变为正数就需要使用以下的公式进行修正

a%p -> (a%p+p)%p

了解了以上的知识点接下来就来学习如何进行算法的优化,在此和之前求和为k的子数组的个数类似还是使用前缀和加哈希表来解决。

在此当前缀和为x时,到子数组最后一个元素前的和为sum时,若此时的子数组内的元素和能被k整除,则此时该子数组的元素和为sum-k

此时由于(sum-x)%k -> sum%x=sum%k,因此我们创建的哈希表当中就存储的是前缀和模k的余数,之后在每个子数组数组当中只要在哈希表当中能找到sum%k的元素就将其对应的值加到最终的总和之上。在此也是要先预处理处理哈希表当中值为0的情况,将hash[0%k]=1

代码实现

class Solution { public: int subarraysDivByK(vector<int>& nums, int k) { //创建哈希表hash unordered_map<int,int> hash; //sum为遍历处数组的前缀和,count为满足条件的子数组个数 int sum=0,count=0; //预处理哈希表中值为0的元素 hash[0%k]=1; //遍历数组 for(auto& x:nums) { sum+=x; //取模的结果为负数时进行修正 int t=(sum%k+k)%k; //判断是否存在取模结果为sum%k的值,存在就将其的个数加到count上 if(hash.count(t))count+=hash[t]; //将此时sum%k的结果添加到哈希表当中 hash[t]++; } return count; } }; 

2.5 连续数组

525. 连续数组 - 力扣(LeetCode)

                                                                                                                         

题目解析

通过以上的题目描述就可以看出该算法题要我们实现的是从给定的二进制数组当中找出最长的子数组长度,该数组内满足元素值为1的个数和元素值为0的个数相等。

算法原理讲解

在此保留枚举就不进行讲解,接下来直接来思考如何使用更优的解法。

看到要求数组当中满足条件的数组,你可能会想到使用滑动窗口来解决,但是在此最终得到的子数组需要示内部的值为1的元素和值为0的元素个数相等,此时就存在问题什么时候才要进行出窗口操作,是在子数组内部值为1和0的个数相等吗?仔细思考也不一定是,在一些情况下可能还会使用到出完窗口的元素。

因此通过以上的分析就能知道滑动窗口算法不适用于本道题

这时我们就要思考原数组当中要得到1和0的元素个数相同的子数组,这样的条件很不好进行筛选,是否能进行转化让问题变成我们熟悉的。如果将数组当中值为0的元素值修改为-1原来的判断条件不就变成了要判断给定的数组当中和为0的子数组的个数,这样不就和之前求值为k的子数组那道算法题类似了吗?

因此和之前的算法题类似还是要适用前缀和以及哈希表的来解决,整体的算法思想是创建一个哈希表来存储对应前缀和,再创建一个变量len来记录满足条件的最长子数组长度,sum变量来统计遍历到数组对应位置的前缀和,之后遍历一次数组。

此时问题就来了我们在此创建的哈希表当中的val存什么呢?还是和之前一样是个数吗?

在此由于最后要得到的是满足条件的最长子数组长度,因此哈希表当中的val就不能再存储个数,而是要存储对应前缀和子数组的最后一个元素的下标。

将前缀和存储到哈希表的时机是什么?

还是和之前一样使用完之后再丢到哈希表当中

如果存在重复的<sum,i>还要进行更新吗?

由于我们要得到的是最长的子数组,这就使得要得到满条件的前缀和当中最后一个元素最靠前的,因此在此仅需保存最后一对<sum,i>即可

还有len长度怎么计算?

通过以下的图示就可以看出len=i-j


除此以上的问题之外还有哈希表中k为0的情况下,由于此时的val存储的是数组下标,因此其初始化值就要为-1

代码实现 

class Solution { public: int findMaxLength(vector<int>& nums) { //创建哈希表 unordered_map<int,int> hash; //sum存储到当前下标的前缀和,len为满足条件的最长数组长度 int sum=0,len=0; //初始化值为0的哈希表元素 hash[0]=-1; //遍历数组 for(int i=0;i<nums.size();i++) { //若数组元素值为0将其修改为-1 if(nums[i]==0)nums[i]=-1; sum+=nums[i]; //判断是否存再前缀和为sum if(hash.count(sum))len=max(i-hash[sum],len); //不存在就更新哈希表 else hash[sum]=i; } return len; } };

2.6 矩阵区域和 

1314. 矩阵区域和 - 力扣(LeetCode)

题目解析 

在该算法题当中要我们实现的是从给定的数组当中找出满足条件的子矩阵和,在此有条件如下所示

i - k <= r <= i + k,j - k <= c <= j + k 且(r, c) 在矩阵内。

 这时你可能无法理解题目的要求是什么,那么我们就通过题目的示例进行理解

在示例1当中给定的矩阵如下: 

在此要给矩阵的每个元素都找出满足条件的answer矩阵,其实answer就是围绕原矩阵的元素外围k格的矩阵

矩阵当中第一个元素的answer矩阵如下所示,在此当中answer矩阵元素值和就为12

其他的矩阵元素也是按照以上的形式得到对应的answer矩阵之后得到元素值的总和,以下就不一一列举。

算法原理讲解

在这道题当中最容易想到的算法就是使用暴力枚举的方式来实现,具体的做法就是遍历从上到下从左到右依次矩阵,在每次遍历到数组的元素时,从矩阵对应位置的i-k行、j-k列一直遍历到i+k行、j+k列为止;在此还要处理边界的情况,但上边界或者下边界超出数组的范围时要进行调整。

以上算法确实能解决本道题,但是暴力解法的效率太底了,在最坏的情况下时间复杂度为O(n^3),此时我们就需要想出效率更高的算法

其实在此在矩阵当中我们就可以试着使用二维前缀和来解决

在此和之前学习的二维前缀和模板一样大致分为两边,一是创建一个前缀和数组,二是使用前缀和数组

那么二维前缀和的递推公式还是通过画图来推导 

通过以上的图示就可以推导出递推公式为:

(A+B)+(A+C)+D-A=dp[i-1][j]+dp[i][j-1]+mat[i][j]-dp[i-1][j-1]。 

但是以上的递推公式还是要进行一定的修正,这时因为在之前的二维前缀和模板当中我们创建存储矩阵的数组下标为0的第一行和第一列并并没有实际存储元素,这样就可以使得我们在递推公式当中直接将dp数组当中的下标映射到原数组上,但在本道题当中给定的数组在下标为0的行与列都是有存储实际元素的,这就使得我们不能简单的将dp数组的下标映射到原数组当中。此时dp数组的下标为i时实际上映射的是原数组下标i-1位置。

因此前缀和数组的递推公式修正后如下所示:

(A+B)+(A+C)+D-A=dp[i-1][j]+dp[i][j-1]+mat[i-1][j-1]-dp[i-1][j-1]。
 

预处理完前缀和数组之后接下来我们就要来思考如何使用前缀和数组

在此首先我们要得到每个answer数组的左上边界点和右下边界点,这样就可以使用二维前缀和来求解

如果i-k时为超出原数组的上左上边界,此时最answer矩阵的左上边界就为i-k,但是如果超出了上边界i就为0,如果j-k时为超出原数组的上左上边界,此时最answer矩阵的左上吧边界就为j-k,但是如果超出了上边界j就为0。右下边界的推导类似,如果i+k时为超出原数组的上右下边界,此时最answer矩阵的右下边界就为i+k,但是如果超出了下边界i就为0,如果j+k时为超出原数组的上右下边界,此时最answer矩阵的右下边界就为j+k,但是如果超出了下边界j就为0。

此外还要处理原数组和前缀和数组dp之间的下标映射关系,在此我们得到的i-k、i+k等都是得到在原数组的下标,但是最后使用是在前缀和数组当中这就使得得到的下标都要加一

因此计算出的answer矩阵元素之和的推导公式如下:

注:在此x1,y1分别表示max(i-k,0)+1;max(j-k,0)+1的值
        x2,y2分别表示min(i+k,m-1)+1;min(j+k,n-1)+1的值(m为行数,n为列数)


(A+B+C+D)-(A+B)-(A+C)+A=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1]

代码实现 

class Solution { public: vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) { //创建一个二维的前缀和数组dp 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]+mat[i-1][j-1]-dp[i-1][j-1]; } } // //创建返回的数组ret 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(i-k,0)+1,y1=max(j-k,0)+1; int x2=min(i+k,m-1)+1,y2=min(j+k,n-1)+1; ret[i][j]=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1]; } } return ret; } }; 

以上就是本篇的全部内容了,接下来还会带来更多的优选算法,未完待续……

Read more

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

🧑 博主简介:ZEEKLOG博客专家,「历代文学网」(公益文学网,PC端可以访问:https://lidaiwenxue.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。 🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图 ” 前端异常捕获与统一格式化:从 console.log(error) 到服务端上报 引言 在前端开发中,异常监控是保证应用稳定性的重要一环。当用户遇到页面白屏、功能不可用等问题时,如果能及时收集到详细的错误信息(包括堆栈、

By Ne0inhk
Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲

Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲

Spring Framework 作为 Java 生态中最流行的企业级开发框架,提供了丰富的模块化支持。其中,Spring Web 模块是支撑 Web 开发的基础组件,无论是传统的 MVC 应用,还是 REST API 及微服务架构,都离不开它的核心能力。 本篇文章将深入解析 Spring Web 模块的核心概念、依赖关系、作用及关键组件,并通过实际案例展示如何使用 Spring Web 进行 RESTful API 调用。本文力求内容精炼、干货满满,帮助你掌握 Spring Web 的核心技术点。 文章目录 * 1、Spring-Web 模块介绍 * 1.1、Spring-Web 模块概述 * 1.2、Spring-Web

By Ne0inhk
.NET 的 WebApi 项目必要可配置项都有哪些?

.NET 的 WebApi 项目必要可配置项都有哪些?

目录 一、数据库配置 (一)选择合适的数据库提供程序 (二)配置数据库连接字符串 (三)数据库迁移(以 EF Core 为例) 二、依赖注入配置 (一)理解依赖注入 (二)注册服务 (三)使用依赖注入 三、Swagger 配置 (一)安装 Swagger 相关包 (二)配置 Swagger 服务 (三)启用 Swagger 中间件 四、接口接收和输出大小写配置 (一)接口接收大小写配置 (二)接口输出大小写配置 五、跨域配置 (一)什么是跨域 (二)配置跨域 六、身份验证与授权配置

By Ne0inhk
《前端文件下载实战:从原理到最佳实践》

《前端文件下载实战:从原理到最佳实践》

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[[email protected]] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? * 专栏导航: 码农阿豪系列专栏导航 面试专栏:收集了java相关高频面试题,面试实战总结🍻🎉🖥️ Spring5系列专栏:整理了Spring5重要知识点与实战演练,有案例可直接使用🚀🔧💻 Redis专栏:Redis从零到一学习分享,经验总结,案例实战💐📝💡 全栈系列专栏:海纳百川有容乃大,可能你想要的东西里面都有🤸🌱🚀 目录 * 《前端文件下载实战:从原理到最佳实践》 * 引言 * 一、需求背景与初始实现 * 1.1 业务需求 * 1.2 初始后端实现 * 1.3

By Ne0inhk