【算法】一维前缀和与二维前缀和
牛客DP34一维前缀和
题解
暴力
通过循环进行解决是非常好想的,在单次查询中,只需要通过两个下标的位置进行将这两个下标进行求和即可,在单次局部求和即可,但是时间复杂度是相当高的,时间复杂度位O(n*q)。
前缀和
通过前缀和数组进行前i个值的和进行记录到前缀和数组中,可以将查询优化成O(1)的时间复杂度,直接将时间复杂度从O(n*q)→ O(n)+O(q),这种方式就是用空间换时间具体步骤如下
- 预处理前缀和数组
- 使用前缀和数组
细节问题
为什么要从下标为1的地方进行计数
为了处理边界情况,这种方式是通过辅助节点的形式进行实现的。
#include <iostream>
#include<vector>
using namespace std;
int main()
{
int n=0;
int q=0;
cin>>n>>q;
vector<int> arr(n+1);
for(int i=1;i<n+1;i++)
{
cin>>arr[i];
}
//创建前缀和数组
vector<long long> dp(n+1); //防溢出
for(int i=0;i<n+1;i++)
{
dp[i]=dp[i-1]+arr[i];
}
//使用前缀和数组
int l=0;
int r=0;
for(int i=0;i<q;i++)
{
cin>>l>>r;
cout<<dp[r]-dp[l-1]<<endl;;
}
}
牛客DP35二维前缀和
题解
暴力
在单次查询中,直接通过两层暴力循环进行累加指定矩阵范围内的数据,在单次查询的过程中的事件复杂度是O(m*n),在q次查询中的时间复杂度就是O(m*n*q)。
前缀和
通过前缀和将进行查询的时间复杂度从O(q)优化成O(1),整体的时间复杂度从时间复杂度从O(m*n*q)→ O(m*n)
预处理一个前缀和矩阵
使用前缀和矩阵
#include <iostream>
#include<vector>
using namespace std;
int main()
{
//进行读取数据
int n,m,q;
cin>>n>>m>>q;
vector<vector<long long >>arr(n+1,vector<long long>(m+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>arr[i][j];
}
}
//进行数据的处理
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];
}
}
//进行数据的输出
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;
}
724、寻找中心数组的下标
题解
暴力
通过以数组中的每一个元素作为中心下标进行向左遍历求和和向右遍历求和将这两组和进行比较看看这两个和是否相等,时间复杂度是O(n*n)。
前缀和
通过前缀和数组进行优化
通过循环从左向右进行通过前缀和数组进行进行进行记录从0到i-1位置的数组和,再通过另一个循环从右向左进行通过前缀和数组进行记录从n到i+1位置的数组和,最后通过循环找到前缀和数组和后缀和数组的值相同就是数组的中心下标
class Solution {
public:
int pivotIndex(vector<int>& nums)
{
vector<int> p_dp(nums.size());
vector<int> n_dp(nums.size());
//处理前缀和数组
for(int i=1;i<nums.size();i++)
{
p_dp[i]=p_dp[i-1]+nums[i-1];
}
//处理后缀和数组
for(int i=nums.size()-2;i>=0;i--)
{
n_dp[i]=n_dp[i+1]+nums[i+1];
}
//寻找结果
for(int i=0;i<nums.size();i++)
{
if(n_dp[i]==p_dp[i])
{
return i;
}
}
return -1;
}
};
238、除自身以外数组的乘积
题解
暴力
通过循环固定数组中的某一个元素,然后向两侧分别通过一个循环进行将每一侧的数进行相乘然后尾插到数组中。时间复杂度为O(n*n)。
前缀和思想
通过前缀积数组和后缀积数组进行优化,这道题本质就是和上一道题的思路是一摸一样的。通过从左向右进行处理0到i-2位置的元素的积存到前缀积数组的i-1位置,从右向左从n-1位置到i+2位置的积存到i+1中,然后进行遍历前缀积数组和后缀积数组,将前缀积数组某个位置的值就是这个位置之前的元素的乘积,将后缀积数组某个位置的值就是这个位置之后的元素的乘积,将这两个值进行相乘就是除该位置以外的所有数的乘积。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums)
{
vector<int>ret;
vector<long long>p_dp(nums.size());
vector<long long>n_dp(nums.size());
//处理边界情况
p_dp[0]=1;
n_dp[nums.size()-1]=1;
//处理前缀积数组
for(int i=1;i<nums.size();i++)
{
p_dp[i]=p_dp[i-1]*nums[i-1];
}
//处理后缀积数组
for(int i=nums.size()-2;i>=0;i--)
{
n_dp[i]=n_dp[i+1]*nums[i+1];
}
//进行最终处理
for(int i=0;i<nums.size();i++)
{
ret.push_back(p_dp[i]*n_dp[i]);
}
return ret;
}
};
560、和为K的子数组
题解
前情提示:这道题非常的有东西
暴力
直接两层循环进行遍历数组进行暴力枚举将所有情况进行枚举,在暴力枚举的过程中通过if语句进行判断将合适的结果通过计数器进行记录,时间复杂度是O(n*n)。
我在进行解题的过程中出现了一点小插曲,通过暴力思想进行考虑完问题后,直接选择通过滑动窗口对暴力进行优化,但是忽视了这一题是存在0和负数的
前缀和数组思路
通过前缀和数组进行判断在 i 位置之前有多少个位置的前缀和等于sum[ i ]-k,要是通过循环的形式进行判断的话有多少个位置的前缀和等于sum[ i ]-k时间复杂度来到了O(n*n)+O(n)还不如纯暴力的方式,但是通过哈希表进行判断的话有多少个位置的前缀和等于sum[ i ]-k时间复杂度直接就优化成的O(2n),还可以进行优化,不用真的进行创建前缀和数组,直接通过sum来进行代替即可
两个魔鬼细节
- 前缀和加入哈希表的时机:必须进行判断完该位置之前有多少个前缀和等于sum[ i ]-k后,才能将这个位置的前缀和进行放入哈希表中
- 如果整个数组的和为k需要进行将数组进行单独的处理 hash[ 0 ] =1;
为什么要 hash[0] = 1?
如果 sum == k,那么 sum - k == 0,这意味着从数组起始位置到当前元素的这段子数组 本身就是一个和为 k 的子数组。
如果不预先设定 hash[0] = 1,那么 hash.count(sum - k) 查找 sum - k == 0 时就会找不到,从而漏掉这种情况。
举个例子:
nums = {3, 4, 7}, k = 7
初始时 hash = {0:1}
遍历 3,sum = 3,查找 sum - k = -4(找不到),hash = {0:1, 3:1}
遍历 4,sum = 7,查找 sum - k = 0(找到 hash[0] = 1),说明 [3,4] 是一个和为 k 的子数组。
结果 ret = 1。
class Solution {
public:
int subarraySum(vector<int>& nums, int k)
{
int sum=0;
unordered_map<int,int>hash;
hash[0]=1;
int ret=0;
for(auto e:nums)
{
sum+=e; //计算当前位置的前缀和
//在当前位置之前统计前缀和为sum-k的个数
if(hash.count(sum-k))
{
ret+=hash[sum-k]; //统计个数
}
hash[sum]++; //把本次的前缀和丢到哈希表中
}
return ret;
}
};
974、和可被k整除的子数组
题解
暴力
通过两个循环进行进行将数组中的所有情况进行枚举出来,在枚举的过程中通过计数器进行记录。暴力的时间复杂度为O(n*n)。
前缀和数组
这道题和上一道题的思路是一样的,通过哈希和前缀和进行优化
补充知识
通过将问题进行转化,也就是(sum - x)%k=0,其中 x 为符合条件的前缀和,就将问题进行转化成了求符合条件的x 个数,等式进行通过同余定理进行转化成sum%k=x%k在通过修正就将问题进行转化求x%k==sum%就可以进行完成要求。
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k)
{
unordered_map<int,int> hash;
int sum=0;
int ret=0;
hash[0%k]=1;
for(auto e:nums)
{
sum+=e;
int r=(sum%k+k)%k;
if(hash.count(r))
{
ret+=hash[r];
}
hash[r]++;
}
return ret;
}
};
525、连续数组
题解
暴力
关于数组类的问题暴力一般都是直接进行将数组进行循环遍历,通过两个循环进行分别确定子数组的左右两个端点然后通过判断条件看是否满足条件即可。但是在处理条件的时候也是需要进行巧妙处理的,当遍历的位置数组元素是0时将子数组的和加上 -1,当遍历的位置数组元素是0时将子数组的和加上 1,当sum (字数组元素的和是0时)满足题意。这样进行处理是比较简单的,也可以通过两个变量进行处理,0和1通过不同的变量进行标记,最后看两个标记位是否相同也可以进行判断。
前缀和数组
当遍历的位置数组元素是0时将子数组的和加上 -1,当遍历的位置数组元素是0时将子数组的和加上 1,当sum (字数组元素的和是0时)满足题意。通过这种巧妙的处理,就变成和为0的字数组了,这道题就直接变成了 和为K 的字数组那道题了。
class Solution {
public:
int findMaxLength(vector<int>& nums)
{
unordered_map<int,int>hash;
int sum=0;
int count=0;
hash[0]=-1;
for(int i=0;i<nums.size();i++)
{
sum+=nums[i]==0?-1:1;
if(hash.count(sum))
{
count=max(count,i-hash[sum]);
}
else
{
hash[sum]=i;
}
}
return count;
}
};
1314、矩阵区域和
题解
这道题的如上图所示,但是将前缀和数组进行处理完成后不可以直接进行返回,需要进行映射到否符合条件的数组中。
在进行预处理数组的时候,需要进行考虑位置的映射,在普通数组中的( i ,j ) 位置相当于在前缀和数组中的 ( i+1 , j+1 )位置,所以说在进行预处理前缀和数组的时候需要进行处理将 mat[ i ][ j ] → mat[ i-1 ][ j-1 ]。
在进行将前缀和数组进行按照题目中的进行辐射后填入的新数组时,还需要进行考虑映射的问题,因为前缀和数组比要进行返回的数组多了一行一列。
class Solution {
public:
vector<vector<int>> matrixBlockSum(vector<vector<int>>& mat, int k)
{
int n=mat[0].size();
int m=mat.size();
//创建前缀和数组
vector<vector<int>> dp(m+1,vector<int>(n+1));
//预处理前缀和数组
for(int i=1;i<m+1;i++)
{
for(int j=1;j<n+1;j++)
{
dp[i][j]=dp[i][j-1]+dp[i-1][j]-dp[i-1][j-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++)
{
//进行加1处理映射
int x1=max(i-k,0)+1;
int y1=max(j-k,0)+1;
int x2=min(m-1,i+k)+1;
int 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;
}
};