这几天学习了前缀和其中包括一维前缀和,以及二维前缀和。
一维前缀和
题目链接:【模板】前缀和_牛客题霸_牛客网 (nowcoder.com)
首先来看一道题目:
这道题目的要求很简单,首先需要你创建两个变量同时要给这两个变量赋值,而变量n代表的是数组的长度(数组下标从1开始),而q代表的是它询问你的次数,也即是你需要输出答案的次数。同时这个长度为n的数组,也是由题目输入的,在输入完之后还会输入l和r,然后要求你输出从l到r这一段距离元素的和。
暴力解法
暴力解法很简单每次都从l的位置一直加到r的位置,然后进行q次,在最坏的情况下时间复杂度为o(n^3)。因为这种解法肯定是会超时的这里我就不写了。
使用一维前缀和
最好的方法就是使用一维前缀和。即我们提前创建好一个数组这个数组要从1开始,假设现在移动到了这个前缀和数组的第i个位置,那么代表的就是从1位置开始到i位置的所有元素的和。
下面来完善这个一维前缀和数组假设叫dp数组。我们知道的是dp[i],代表从第一个元素开始到第i个元素的元素和。由此就能够得到一个公式dp[i] = dp[i-1]+nums[i]。这里的nums为原数组使用这个数组就能够解决这道题目:下面是代码的实现:
#include<iostream>
#include<vector>
using namespace std;
int main() {
int n = 0, q = 0;
cin >> n >> q;
vector<int> arr(n+1);//创建一个n大小的数组,因为下标从0开始,所以要有下标为n的元素就要有n+1个元素
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
//现在创建一个前缀和数组
vector<long long> dp(n + 1);
dp[0] = 0;
int j = 1;
//这里是使用了动态规划的思想将每一个前缀和,提前先算好
//需要注意的是dp的下标要从1开始不然在计算第一个数的时候,会出现dp[-1]的情况
for (int i = 0; i < n; i++, j++)
{
dp[j] = dp[j - 1] + arr[i];
}
while (q--)
{
int l = 0;
int r = 0;
cin >> l >> r;
cout << (dp[r] - dp[l - 1]) << endl;
}
return 0;
}
二维前缀和
题目链接:【模板】二维前缀和_牛客题霸_牛客网 (nowcoder.com)
这道题目和上面拿到题目是一样的只是要求变化了,首先这一次是要求我们在一个矩阵中寻找从一个坐标开始到另外一个坐标之间的距离:可以使用下面的三个图去理解。
下面是使用这个二维前缀和:
由此我们就能够写出代码了:
#include <iostream>
#include<vector>
using namespace std;
int main() {
int n = 0,m = 0,q = 0;//创建能够赋值的变量
cin>>n>>m>>q;
vector<vector<int>> arr(n+1,vector<int>(m+1));//创建n行m列的二维数组
for(int i = 1;i<=n;i++)
{
for(int j = 1;j<=m;j++)
{
cin>>arr[i][j];
}//给整个数组从1,1开始赋值
}
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]-dp[i-1][j-1]+arr[i][j];//使用抽象求面积法来理解
//dp数组的求取
}
}
//最后是使用dp数组
int x1 = 0,y1 = 0,x2 = 0,y2 = 0;//
while(q--)
{
cin>>x1>>y1>>x2>>y2;
//这里依旧是使用抽象法去使用dp数组
cout<<dp[x2][y2] - dp[x1 - 1][y2] - dp[x2][y1-1]+dp[x1-1][y1-1]<<endl;
}
}
下面是应用一维前缀和的题目:
一维前缀和题目
题目1
题目1链接:724. 寻找数组的中心下标 - 力扣(LeetCode)
这道题目应用一维前缀和的思想是很容易就能够解决的,假设现在我们在数组中选到了下标为i的元素,那么我们怎么判断这个i,前面的元素和是否等于后面的元素和呢?很容易那就是将i前面的元素和提前算出来,后面的元素也要提前算出来。
dp[i] = dp[i-1] +nums[i-1];//因为我们在算i位置的前缀和的时候是要算到i-1之前的元素,
dp2[i] = dp[i+1] +num[i+1];//这里算的就是从i+1开始到第n-1个元素的后缀和。
最后判断dp[i]是否等于dp2[i]如果相等那么这就是一个满足条件的下标。
下面是代码实现
class Solution {
public:
int pivotIndex(vector<int>& nums) {
int n = nums.size();
//题目的解决方法很简单,创建一个前缀表f,一个后缀表g,其中前缀表f[i]表示为从数组起点开始到第i-1个元素的和
//那么f(i) = f(i-1)+nums[i-1];故要单独对f(0)做处理,不然会出现数组越界访问
//同理g也是一样的,只不过g是从最后一个元素开始到第i+1个元素
vector<int> f(n);//创建一个前缀表
vector<int> g(n);//创建一个后缀表
f[0] = 0;
g[n-1] = 0;
//下面开始从左往右给f填表
//从1开始因为如果从0开始i-1会越界
for(int i = 1;i<n;i++)
{
f[i] = f[i-1]+nums[i-1];
}
//下面开始从右往左给g填表
//从n-2开始因为如果是n-1那么在下面的i+1就会越界
for(int i = nums.size()-2;i>=0;i--)
{
g[i] = g[i+1]+nums[i+1];
}
for(int i = 0;i<nums.size();i++)
{
if(f[i] == g[i])
{
return i;
}
}
return -1;//不存在中心下标
}
};
题目2
题目二:除自身以外数组的乘积
题目链接:238. 除自身以外数组的乘积 - 力扣(LeetCode)
思路和上面的那道题目一摸一样,假设你要求第i个位置的目标值,那你提前将这个i之前所有元素的乘积都放到了dp[i],而i之后所有元素的乘积都放到了dp2[i],那么des[i] = dp[i]*dp2[i]。下面要解决的就是dp和dp2的填写问题。
dp[i] = dp[i-1]*nums[i-1]。//原因是题目要求不包含本身
dp2[i] = dp2[i+1]*nums[i+1]。//原因同上
最后可以写出代码:
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
//思路和前面一道差不多,求取元素i的前缀积和后缀积
int n = nums.size();
vector<int> f(n);//创建一个前缀积的数组,其中的f[i]为从首元素开始到i-1元素的乘积
f[0] = 1;//处理边界情况,这里不能初始化为0,否则会出现全0的现象,下面一样
vector<int> g(n);//创建一个后缀积的数组//这里的g[i]表示的就是从末尾元素开始到i+1元素的乘积
g[n - 1] = 1;//处理边界情况
//从左往右給前缀积求值
for(int i = 1;i<n;i++)
{
f[i] = f[i-1]*nums[i-1];//这里的f[i-1]求得是从1开始到i-2处元素的乘积
}
for(int i = n-2;i>=0;i--)
{
g[i] = g[i+1]*nums[i+1];//这里的g[i+1]求得是从i+2处到最后的乘积乘上一个nums[i+1]
}
vector<int> answer(n);//创建答案返回数组
for(int i = 0;i<n;i++)
{
answer[i] = f[i]*g[i];
}
return answer;
}
};
题目3
和为k的子数组题目链接:LCR 010. 和为 K 的子数组 - 力扣(LeetCode)
题目思路:
下面就是代码实现:
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
//这道题目的解题思路不容易思考要解出这道题目需要做以下的处理
//假定在数组中已经选取了一个下标为i的位置,然后我们选择去寻找zai这个i
//位置之前是否存在一个前缀和为sumi(到i位置的前缀和)-k,如果存在则从存在的那个位置到i
//位置的前缀和恰好就是一个连续并且he为k的子数组,为了能够快速判断在i的前方是否真的存在过sumi-k
//需要使用哈希表
unordered_map<int,int> hash;
int sum = 0;//储存当前位置的前缀和
int ret = 0;//确定返回的数目
//当然可能存在一种特殊情况那就是sumi刚好就是k,在这种情况下是不存在sumi-k的当时确实是存在符合条件的情况
//所以提前将hash[0]赋值为1
hash[0] = 1;
for(int i = 0;i<nums.size();i++)
{
sum+=nums[i];//确定当前的前缀和
if(hash.count(sum-k))//存在过hash[sum-k]
{
ret+=hash[sum-k];//将答案更新到ret中
}
//不要忘记将当前的sum更新到哈希表中去
hash[sum]++;
}
return ret;
}
};
题目4
和可被k整除的子数组题目链接:974. 和可被 K 整除的子数组 - 力扣(LeetCode)
下面是代码实现:
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
//这道题目其实考察的是一个同余定理(a-b)%k = q(整数)则可以得到a%k = b%k
//依旧假设现在存在一个i知道了当前的前缀和为sumi,那么假设在i之前的某个前缀和为sum,并且在sum到sumi之间是存在
//满足条件的子数组那么接下来就有了(sumi - sum)%k = 0。到了这一步就很简单了
//我们只需要判断在sumi之前是否存在过sum%k是等于sumi%k的。就存在满足答案的子数组了
unordered_map<int,int>hash;//储存的是前缀和的余数
int sum = 0;//储存前缀和
int ret = 0;//返回答案的个数
hash[0%k]++;//解决特殊情况
for(int i = 0;i<nums.size();i++)
{
sum+=nums[i];//记录当前的前缀和
if(hash.count((sum%k+k)%k))//判断是否存在
{
ret+=hash[(sum%k+k)%k];//更新答案
}
hash[(sum%k+k)%k]++;
}
return ret;
}
};
题目5
题目连续数组。题目链接:LCR 011. 连续数组 - 力扣(LeetCode)
这道题目如果直接写的话是很不好写的。但是如果将nums中的数字0修改为-1。那么这道题目也就变成了。求取一段子数组和为0的最长长度。那么这道题目也就可以变成和上面那两道题目一样的思路了。如果求出了一个sumi。然后再i之前存在一个sumj等于sumi自然就是再j和i之间的和为0,也就是在j到i的区间内0和1的数量是相等的。但是这道题目并不是要求我们求具有相同0和1的子数组的数量。而是最长子数组的长度。那么在使用哈希表储存的时候,哈希表里面储存的就是前缀和以及这个前缀和对应的下标。但是在将前缀和储存进哈希表的时候,如果这个前缀和在之前已经储存进哈希表中了。那么现在就没有必要进入哈希表了。因为题目要求的是最长的长度。当然还是存在sumj刚好等于0的。那么在这个时候i就要让hash[0] = -1。因为求长度为right - left。left肯定就是hash[0]的数值,所以要让hash[0] = -1。
下面是代码实现:
class Solution {
public:
int findMaxLength(vector<int>& nums) {
//题目思路和和为k的子数组是一样的
unordered_map<int, int> hash;//哈希表里面储存的前面的int为前缀和,后面的int为这个前缀和的下标,因为这道题目需要的是长度
//而不是这个sum出现的次数-1
int sum = 0;
int len = 0;
hash[0] = -1;
for (int i = 0; i < nums.size(); i++)
{
sum += nums[i] == 0?-1:1;//记录当前的前缀和,如果现在这个加的值为0那么要改为加成的是一个-1
if (hash.count(sum))//此前存在过一次sum
{
len = max(len, i - hash[sum]);
}
else
{//这里的意思就是当我们在之前便遇到了一次哈希表中存在过sum了
//那么就没有必要将这个值存入到哈希表中去,因为这道题目要求的是我们能够求出最长的连续子数组
//而不是最短的
hash[sum] = i;//储存下标
}
}
return len;//最后返回这个长度
}
};
二维前缀和题目:
题目
矩阵区域和:题目链接:1314. 矩阵区域和 - 力扣(LeetCode)
那么下面就是填写二维前缀表(创建的大小要比原矩阵大一圈,否则会出现很多的边界问题)。因为这道题目的的原矩阵下标是从0开始的而二维前缀和的下标是从1开始的所以要注意映射问题。
下面是代码实现:
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k) {
//这道题目是二维前缀和的创建和使用
//第一步要将mat数组的前缀和全部给求出来
int m = mat.size();//这个是二维数组的行数
int n = mat[0].size();//这个是二维数组的列数
vector<vector<int>> dp(m+1,vector<int>(n+1));//创建一个大一圈的dp数组为了处理边界情况
//下面给dp数组赋值
dp[1][1] = mat[0][0];//给dp数组初始化
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];//这里涉及到了一个从前缀和数组到原始数组的映射问题
//第i,j的位置对应的原数组应该是i-1,j-1
}
}//由此便创建完成了一个dp数组(前缀和数组)
//下面是使用并且创建返回数组
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)+1;
int y1 = max(0,j-k)+1;//因为可能在寻找左上端点的时候会造成越界所以要和0比较,如果小于0就不是
int x2 = min(m-1,i+k)+1;//因为这里从answer数组映射到前缀和数组所以要让x和y都加上1,因为前缀和数组是从1开始的
int y2 = min(n-1,j+k)+1;//由此就得到了answer数组里的这个位置需要求的和是哪两个坐标之间的面积
//这里同样是不能够越过最右下角的
answer[i][j] = dp[x2][y2] - dp[x1-1][y2] -dp[x2][y1-1]+dp[x1-1][y1-1];
}
}
return answer;
}
};
因为我的水平有限所以如果发现了任何错误,欢迎指出我一定修改。