模板一维前缀和
【模板】前缀和_牛客题霸_牛客网
该算法是先预处理一个数组,用空间换时间,将原本时间复杂度为O(n2)降为O(n)
题目解析
题中下标(用i表示)从1开始计数,长度为n的数组,想访问到an 位置,创建数组时要创建大小为n+1的数组
算法原理
-
解法一:暴力解法——模拟:题中q次询问,每次询问按要求从头遍历即可,时间复杂度为O(n*q),根据题中的数据范围,大概为O(1010)
-
解法二:前缀和——快速求出数组某一连续区间的和(快速时间复杂度O(1),相比解法一遍历O(n)快许多),整体时间复杂度O(q)+O(n)。这个O(n)是我们在预处理前缀和数组时需要遍历一遍原数组。
-
先预处理出一个前缀和数组,先创建一个和原始数组同规模的数组dp(本质上是一个小的动态规划)。dp数组中某一个位置的元素dp[i]表示[1,i]区间所有元素的和。
-
使用前缀和数组,例如求[l,r]区间的和时,我们只需要求出紫色线区间的和,减去绿色直线的和即可,即dp[r]-dp[l-1];因为我们上一步求出了dp的数组,所以这一步的时间复杂度为O(1),这也是相比暴力解法快速的原因。
-
因为[1,r]这段区间的和与[1,l]这段区间的和本质上是同一类问题,当我们研究同一类问题时,我们可以把这些同一类问题抽象成状态表示,进而用动态规划的思想解决。
- 细节问题——为什么下标要从1开始计数?
如果我们要访问[0,2]区间的数组时,根据上面总结的我们需要访问dp[2]和dp[-1]这两段区间的数组和,但是-1这个位置访问不到,此时需要处理边界情况。但我们从下标1开始计数不会有问题(dp[0]置为0即可,不影响前缀和)
代码实现
#include <iostream>
#include<vector>
using namespace std;int main()
{//1.读取数据int n,q;cin >> n >> q;vector<int> arr(n+1);for(int i = 1;i <= n;i++) cin >> arr[i];//2.预处理前缀和数组vector<long long> dp(n+1); //防止溢出for(int i=1;i<=n;i++) dp[i] = dp[i-1]+arr[i];//3.使用前缀和数组int l=0,r=0;while(q--){cin >> l >> r;cout << dp[r] - dp[l-1] << endl;}return 0;
}
模板——二维前缀和
【模板】二维前缀和_牛客题霸_牛客网
题目解析
算法原理
解法一:暴力解法——模拟
时间复杂度O(mnq)
解法二:前缀和(时间复杂度O(m*n)+O(q))
-
预处理出前缀和矩阵(这里遍历矩阵时间复杂度O(m*n))
- 这里我们求dp[i][j]如果还像上个题一样从头开始遍历,那创建dp表所用的时间复杂度一定非常高。因此我们先找个规律,快速求出dp[i][j]的值此时我们把这个图抽象出来。我们把arr的值划分为四个部分:A部分的面积可以表示为[1][1]到[i-1][j-1](该元素为右下角)这块区间的和,但我们发现这样直接一块块的算BCD非常麻烦,所以我们可以转化成A+B([1][1]到[i-1][j]) A+C([1][1]到[i][j1]) D(arr[i][j]) -A([1][1]到[i-1][j-1])。这样我们求dp[i][j]时直接套用公式,用O(1)的时间复杂度求出,当求整个dp矩阵时只需遍历一遍矩阵就可以全部求出dp矩阵。
-
使用前缀和矩阵(每一次求消耗时间复杂度O(1),一共q次即为O(q))
题中要求求出[x1][y1]到[x2][y2]区间和时,此时我们还可以将他划分为四个部分,即区间D为题中要求。我们可以先把图中整个和求出来(A+B+C+D)然后减去其他部分面积。求得公式后,我们接下来求区间就可以用O(1)的时间复杂度求
- 细节问题:和一维那里一样。我们要在矩阵的最上⾯和最左边添加上⼀⾏和⼀列 0,这样我们就可以省去⾮常多的边界条件的处理(可以⾃⾏尝试直接搞出来前缀和矩阵,边界条件的处理会让你崩溃的)。处理后的矩阵就像这样:
这样,我们填写前缀和矩阵数组的时候,下标直接从 1 开始,能⼤胆使⽤ i - 1 , j - 1 位置的值。
注意: dp 表与原数组内的元素的映射关系:
i. 从 dp 表到原矩阵,横纵坐标减⼀;
ii. 从原矩阵到 dp 表,横纵坐标加⼀
代码实现
#include <iostream>
using namespace std;
const int N = 1010;
int arr[N][N];
long long dp[N][N];
int n, m, q;
int main()
{cin >> n >> m >> q;// 读⼊数据for (int i = 1; i <= n; i++)for (int j = 1; j <= m; j++)cin >> arr[i][j];// 处理前缀和矩阵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, y1, x2, 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;
}
寻找数组的中心下标
寻找数组的中心下标
题目解析
- 数组** 中心下标 **是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
- 如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
- 如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。
算法原理
- 解法一:暴力解法:先算出[0,i-1]这个区间的和,在算出[i+1,n-1]这段区间的和,比较两者是否相等。每次枚举一个中心下标,就让左边加一遍,右边加一遍。时间复杂度O(n2)
- 解法二:前缀和:刚好前缀和是用来记录某一段连续区间的和。这里我们用f记录前缀和数组,后g表示后缀和数组。
- f[i]表示:[0,i-1]区间的和(不要死记模板,要根据具体情况具体分析);g[i]表示:[i+1,n-1]区间的和
- 预处理前缀和数组和后缀和数组:f[i] = f[i-1]+nums[i-1] g[i]=g[i+1]+nums[i+1] 后缀数组倒着填(f[i-1]表示0~i-2区间的和 g[i+1]表示从i+2~n-1区间的和)
- 从0~N-1枚举所有的中心下标i,然后判断f[i]是否等于g[i].(因为如果有多个中心下标,要选出左边的)
- 细节问题:
- f[0]和g[n-1]要特殊处理,因为两种情况是最左边和最右边。各有一边的和为0,所以将两端数字都置为0,不影响求和
- 填表顺序f要从左向右。g要从右向左
代码实现
class Solution {
public:int pivotIndex(vector<int>& nums){int n=nums.size();vector<int> f(n),g(n);//1.预处理前缀数组和后缀数组for(int i=1;i<n;i++)f[i] = f[i-1] + nums[i-1];for(int i=n-2;i>=0;i--) //倒着填,最后一个位置是n-1,但是g[n-1]位置上的值是0就可以,n-1会越界g[i] = g[i+1]+nums[i+1]; //2.使用for(int i=0;i<n;i++)if(f[i] == g[i])return i;return -1; }
};
出自身以外数组的乘积
除自身以外数组的乘积
题目解析
- 除自身以外其他数的乘积
- 不能使用除法
- 时间复杂度要求O(n)
算法原理
- 暴力解法:边枚举位置,边计算乘积。时间复杂度O(n2)
- 前缀和:利用“前缀和”数组和“后缀和”数组计算除自身位置两边的数的乘机
- 预处理数组:f表示前缀积,f[i]:我们只需要考虑0i-1区间数字的乘积。当我们求0i-1这段区间的乘积时,我们已经知道0~i-2这段区间的乘积,即f[i-1],则f[i] = f[i-1]*nums[i-1]
g表示后缀积,g[i]表示i+1n-1这段区间的积,同理想求这段区间的乘积时,我们已经直到了i+2n-1这段区间的乘积(即g[i+1]已知)则g[i] = g[i+1]*nums[i+1]
- 使用:先创造一个和原始数组同规模的数组ret(也就是dp)
- 细节问题:f(0)与g(n-1)设置为1,与上道题同理,只不过这题是乘积,不能变为0
代码实现
class Solution {
public:vector<int> productExceptSelf(vector<int>& nums) {int n=nums.size();vector<int> f(n),g(n);//1.预处理前缀数组和后缀数组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--) //倒着填,最后一个位置是n-1,但是g[n-1]位置上的值是1就可以,n-1会越界g[i] = g[i+1]*nums[i+1]; //2.使用vector<int>ret(n);for(int i=0;i<n;i++)ret[i] = f[i]*g[i];return ret; }
};
和为k的子数组
和为k的子数组
题目解析
- 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 _该数组中和为 k** **的子数组的个数 _。
- 子数组是数组中元素的连续非空序列。
- 数组中数字有正有负
算法原理
- 暴力解法:固定一个位置,开始枚举,一直加,直到和等于k,但这时不能停止,因为数字有正有负,后面可能抵消。再次统计,直到加到最后为止。时间复杂度O(n2)
- 前缀和+hash:这里我们引入一个以i位置为结尾的所有子数组,这样我们求和为k的子数组问题就转换为了在[0,i-1]区间内有多少个前缀和为sum[i]-k的子数组。但如果这样就开始遍历,那时间复杂度为O(n2)+O(n)甚至还不如暴力解法。我们要思考如何快速找到前缀和的数组有多少个等于他,借助数据结构哈希表,将前缀和塞进哈希表里,统计出现的次数,这样就不用遍历前缀和数组,只需要在哈希表中找到它出现的次数即可
- 细节问题:
- 前缀和加入hash表的时机:
- 把前缀和全算出来,然后放入hash表中(不可以),我们是要找i位置之前的,如果全放进哈希表中,我们可能会统计i位置之后的值,他们的前缀和也刚好等于k,此时会重复计数。所以在计算i位置之前,hash表里只保存[0,i-1]位置的前缀和。
- 我们不用真的创建一个前缀和数组
- dp[i]=dp[i-1]+nums[i],计算dp[i]前缀和时,我们只需要直到dp[i-1]区间和就行,不需要记录dp[i-2],dp[i-3]的值,所以我们可以用一个sum来i位置之前的和,当每次计算完之后,sum更新dp[i]即可。
- 如果整个前缀和等于k呢?
- 如果有一种情况,是当枚举到i位置时发现整个数组的和等于k,那我们就需要去[0,-1]这段区间找和为0,但这个区间不存在呀,那我们就需要先放一个hash[0] = 1,即默认有一个前缀和等于0
- 前缀和加入hash表的时机:
注意:此题不能用滑动窗口的方法解决,因为当定义left和right进行移动时,由于数组中有正有负的情况,可能存在前面有段区间的负数和与后面区间正数和相抵消的情况,但以为此时left和right都向右移动,就会漏掉这种情况,不符合单调性这一性质。
代码实现
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;}
};
和可被K整除的⼦数组
和可被K整除的⼦数组
题目解析
返回其中元素之和可被 k 整除的(连续、非空) 子数组 的数目。
算法原理
-
暴力枚举:枚举出所有子数组,求和判断是否能被k整除。
-
**前缀和+hash:**找出两个前缀和一个以i为结尾的前缀和sum;另一个标记为x。根据题目要求,我们有(sum-x)%k=0,根据同余定理可得,sum%k等于x%k。所以此时问题转换为只需要在[0,i-1]区间找有多少个前缀和余数等于sum%k. 同时根据C++负数%正数,我们需要将其修正为(sum%k+k)%k。并且该题并不需要真的创建一个前缀和数组,因为我们只需要记录前缀和的余数即可。此时我们创建一个hash<int,int>,第一个存前缀和余数,第二个存出现的个数。(这里需要注意的细节问题和上道题一样)
补充知识:
- **同余定理:**如果(a+b)➗p=k…0(即a+b能被p整除),则a%p 等于b%p
- **C++中【负数%正数】的结果以及修正:**负数%整数=负数,如果想将结果修正成正数,变成a%p+p,为了正负统一,则(a%p+p)%p
注:该题依然不能用滑动窗口思想来解题,因为有可能出现负数或者0的情况。
代码实现
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 x : nums){sum += x; // 算出当前位置的前缀和int r = (sum % k + k) % k; // 修正后的余数if(hash.count(r)) ret += hash[r]; // 统计结果hash[r]++;}return ret;}
};
连续数组
连续数组
题目解析
找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
算法原理
如果我们直接去统计0和1出现的个数,这样难度会有点大,不妨我们转换一下思路,我们把0全部变成-1,那就转化为在数组中找出最长的子数组,找出和为0即可。我们之前做过一道和为k的子数组,这样就容易一点。
前缀和+hash:这里思路和和为k的子数组一样,这里主要考虑一些细节问题:
- **hash表里存什么:**因为题中要找出最长的子数组,所以hash<int,int>,第一个存前缀和,第二个存下标,因为我们要统计长度
- **什么时候存入hash表:**当前位置的值,和当前位置所绑定的前缀和用完之后再存入
- **如果有重复的前缀和与下标,怎样存:**保留前面的<sum,j>这样能够保证子数组到i位置长度最长
- **默认前缀和为0:**当我们发现整个数组和为0的时候,我们需要在下标-1的位置(前几道题是置为0,因为要统计和或者乘积,这道题我们需要记录下标从而统计长度)所以hash[0]=-1
- **如何计算长度:**计算i到j的距离我们公式是i-j+1,在绿色标记的区间长度中实际上是不包含j这个点的,所以我们算多了一个,要减去1,即为i-j
代码实现
class Solution {
public:int findMaxLength(vector<int>& nums) {unordered_map<int, int> hash;hash[0] = -1; // 默认有⼀个前缀和为 0 的情况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;}
};
矩阵区域和
矩阵区域和
题目解析
answer矩阵中每一个位置返回的值是原矩阵中以该位置为中心,上下左右同时扩展k个格子,所组成的矩阵的和,填入answer,如果超出矩阵范围不计算,此时answer[0][0]位置为12
算法原理
本质是快速求出矩阵某一范围的和,用二维前缀和。
- ret表示所求阴影面积,我们用总面积-(A+B)-(A+C)+A得出(这是动态规划算法里的状态转移方程)
-
接下来处理扩大k个格子坐标的越界问题
-
处理下标映射关系问题:在我们的dp矩阵中,我们为了方便处理边界情况,我们是让下标从1开始,但是leetcode中的下标是从0开始。
注意: dp 表与原数组内的元素的映射关系:
i. 从 dp 表到原矩阵,横纵坐标减⼀;
ii. 从原矩阵到 dp 表,横纵坐标加⼀
在第一步求坐标的时候直接+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));// 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];// 2. 使⽤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;}
};