目录
和为K的子数组
和可被K整除的子数组
连续数组
矩阵区域和
和为K的子数组
题目
思路
我们可能想到的是,从头到尾扫描数组,然后分别计算以该位置为开始,一直到数组末尾,符合和为K的子数组,但是这种方法的时间复杂度是O(N^2),是会超时的,因此需要别的方法来解决。
可能又会想到利用“前缀和”的思想,但是从头到尾扫描整个数组,然后分别计算以该位置为开始,一直到数组末尾,符合和为K的子数组,这样时间复杂度依旧是O(N^2),是会超时的,因此需要别的方法来解决。
下面我们仔细分析一下,子数组问题可以通过以某个位置为结尾来分析,此时可以先计算出该位置(含该位置)之前所有数的和,这个和是确定的,要想找到以该位置为结尾且和为K的子数组,只需要找出该位置之前和为sum-K的子数组即可,该位置之前和为sum-K的子数组的数量,就是以该位置为结尾且和为K的子数组的个数,这里可以使用“前缀和”思想,不过这里存储的不再单单只是元素的和,而是每个元素的和以及对应的个数,这里需要注意的一点是,我们并不是提前计算好前缀和,而是分析到哪个位置就计算到哪个位置之前的前缀和以及其对应的个数,可以使用哈希表进行存储,方便及时查询到,并且我们可以做一些优化,因为计算前缀和只需要知道前一个位置的前缀和,因此使用一个变量就可以实现计算前缀和。
还有需要注意的是,针对上面的方法,如果所求整个数组的和为sum,那么我们会计算[0,-1]区间的数组的和,因此,我们需要考虑到这一点,处理方法是先将0存放到哈希表中,映射的值是1.
代码
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> hash;
hash[0]=1;
int sum=0,ret=0;
for(int x:nums){
sum+=x;
if(hash.count(sum-k)) ret+=hash[sum-k];
hash[sum]++;
}
return ret;
}
};
和可被K整除的子数组
题目
思路
我们可能想到的是,从头到尾扫描数组,然后分别计算以该位置为开始,一直到数组末尾,符合和可被K整除的子数组,但是这种方法的时间复杂度是O(N^2),是会超时的,因此需要别的方法来解决。
可能又会想到利用“前缀和”的思想,但是从头到尾扫描整个数组,然后分别计算以该位置为开始,一直到数组末尾,符合和可被K整除的子数组,这样时间复杂度依旧是O(N^2),是会超时的,因此需要别的方法来解决。
下面我们仔细分析一下,子数组问题可以通过以某个位置为结尾来分析,此时我们可以先计算该位置(含该位置)之前所有元素的和,题目要求是找出和可被K整除的子数组,因为该位置(含该位置)之前所有元素的和是确定的,如果找出以该位置为结尾,和可被K整除的子数组,假设该位置(含该位置)之前所有元素的和是sum,除了和可被K整除的子数组之外元素的和是sum-n*k,那么根据《同余定理》就有sum%k=(sum-n*k),即我们只需找出该位置之前余数为sum%K的子数组的个数即可,这里需要注意的是,我们不再是先提前计算好前缀和,而是求到哪个位置就计算到哪个位置的前缀和,这里使用变量来代替之前的数组,因为计算前缀和只会用到前一个位置的前缀和和当前位置的元素,并且这里不再只是计算前缀和,还需要计算每个前缀和的值的余数对应的个数,使用哈希表来进行存储,这样就可以更快地查询到每个前缀和的值的余数对应的个数。
因为C++中对负数取模,得到的还是一个负数,因此我们需要做一些处理,即针对sum%K,变换成(sum%K+K)%K。
还有需要注意的是,针对上面的方法,如果所求整个数组的和为sum,那么我们会计算[0,-1]区间的数组余数为(sum%K+K)%K的个数,因此,我们需要考虑到这一点,处理方法是先将0存放到哈希表中,映射的值是1.
代码
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
unordered_map<int,int> hash;
hash[0%k]=1;
int sum=0,ret=0;
for(int x:nums){
sum+=x;
int a=(sum%k+k)%k;
if(hash.count(a)) ret+=hash[a];
hash[a]++;
}
return ret;
}
};
连续数组
题目
思路
我们可能想到利用“前缀和”的思想,但是这道题是01序列,利用“前缀和”思想并不能知道具体有多少个0,因此我们需要再分析分析,我们不妨将原始数组中的0替换成 -1,这样找出的数组是1和 -1能够相互抵消的。
解决这道题时,我们使用“前缀和”时,不能先提前计算好前缀和,而是分析到哪个位置就把前缀和计算到哪个位置,因为计算每个位置的前缀和只会用到前一个位置的前缀和以及当前位置的值。
需要注意的是,因为题目要求含有相同数量的01的子数组的最长长度,因此下面在计算前缀和时还需要记录每个前缀和值对应的区间末尾最小下标,即如果再次遇到相同前缀和值的时候,不再记录,只记录前缀和值首次出现的区间末尾下标。
代码
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;
if(hash.count(sum)) ret=max(ret,i-hash[sum]);
else hash[sum]=i;
}
return ret;
}
};
矩阵区域和
题目
思路
这道题如果使用暴力解法的话,即针对针对查询,算出本次查询的区域,然后再计算出该区域所有数的和,但是这种方法的时间复杂度是O(N*M*Q),是会超时的,因此需要别的方法来解决。
下面将依旧使用“前缀和”的思想来解决这道题,即预先计算出每个位置和左上角构成的矩形的值的总和,针对每次查询,就可以使用O(1)的时间复杂度计算出所查询区域的值的总和。
【1】计算好每个位置和左上角构成的矩形的值的总和
【2】计算出所查询区域的值的总和
其实快速解决这道题是利用二维前缀和,只不过这道题要处理一些边界情况,因为下标[i-k],[i+k],[j-k],[j+k]是有可能超过二维前缀和矩阵的大小的,需要特别处理一下。
代码
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));
vector<vector<int>> answer(m,vector<int>(n));
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];
for(int i=0;i<m;i++)
for(int j=0;j<n;j++){
int x1=max(1,i-k+1),y1=max(1,j-k+1);
int x2=min(m,i+k+1),y2=min(n,j+k+1);
answer[i][j]=dp[x2][y2]-dp[x1-1][y2]-dp[x2][y1-1]+dp[x1-1][y1-1];
// answer[i][j]=dp[min(m,i+k+1)][min(n,j+k+1)]-dp[max(0,i-k)][min(n,j+k+1)]-dp[min(m,i+k+1)][max(0,j-k)]+dp[max(0,i-k)][max(0,j-k)];
}
return answer;
}
};