一、和为K的子数组
题目链接:
题目介绍:
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数。
子数组是数组中元素的连续非空序列。
1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107
思路:
对于暴力解法来说,我们需要确定一个起始下标,然后向后遍历,如果碰到一个区间之和为K了,记录一下,但是此时还需要向后遍历,因为数组中的元素可能存在0和负数,这样暴力解法的时间复杂度为O(n^2).
如果我们可以创建出一个前缀和数组,dp[i]表示0~i位置的和。那我们就可以通过访问一遍数组下标,以i为数组终点,我们要找一个下标到i位置的和为k,那0到这个下标之前的一个元素的和就是的dp[i]-k。
也就是说,我们要找以i位置为终点,找i位置前有多少个前缀和等于dp[i]-k。但是仔细想一下,创建前缀和数组时间复杂度为O(n),遍历每一个下标时,还需要遍历它之前的前缀和数组,这样的时间复杂度为O(n^2),看起来比暴力解法还复杂。
其实我们只关心 i 位置之前,所以我们并不需要真正创建一个前缀和数组,而且我们也没必要遍历前缀和数组,只需要创建一个哈希表,维护i位置之前前缀和与个数的映射即可,这个过程可以一遍求前缀和,一遍建立映射。
hash[0]=1, 是因为 前缀和 为0的默认就有一个。假设0~i这段区间的和正好为k,那我们就要去(0~-1)这个区间找前缀和为0的个数了,但是这个区间是不存在的,为了加上这个情况,所以将hash[0]设置为1
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> hash;
hash[0]=1;
int sum=0;
int ret=0;
for(auto e:nums)
{
sum+=e;
if(hash.count(sum-k))
ret+=hash[sum-k];
hash[sum]++;
}
return ret;
}
};
二、和可被K整除的子数组
题目链接:
974. 和可被 K 整除的子数组 - 力扣(LeetCode)
题目介绍:
给定一个整数数组 nums
和一个整数 k
,返回其中元素之和可被 k
整除的非空 子数组 的数目。
子数组 是数组中 连续 的部分。
1 <= nums.length <= 3 * 10^4
-104 <= nums[i] <= 10^4
2 <= k <= 10^4
思路:
在写这个题之前我们需要知道两个知识:
- 同余定理: (a-b)/p=k .....0 ---> a%p == b%p。用文字叙述就是,如果两个数相减的差能被 n 整除,那么这两个数对 n 取模的结果相同。
- c++修正负数%正数为负数: (a%p+P)%p 【加P是为了让他变为正数,在取模是为了本来是正数的受影响】
同余定理验证:
了解了同余定理,再看这个题目,我们要找一个区间(x~i)的和可以被k整除,假设0~x-1区间的和为a, x~i区间的和为b,题目的要求也就是 (b-a)%k=0.
由同余定理可得, [0, x - 1] 区间与 [0, i] 区间内的前缀和同余。于是问题就变成:
找到在 [0, i - 1] 区间内,有多少前缀和的余数等于 sum[i] % k 的即可
class Solution {
public:
int subarraysDivByK(vector<int>& nums, int k) {
//同余定理: (a-b)/p=k .....0 -> a%p == b%p
//c++修正负数%正数为负数: (a%p+P)%p
unordered_map<int,int> hash;
hash[0]=1;
int sum=0,ret=0;
for(auto e:nums)
{
sum+=e;
int r=(sum%k+k)%k;
if(hash.count(r))
ret+=hash[r];
hash[r]++;
}
return ret;
}
};
三、连续数组
题目链接:
题目介绍:
给定一个二进制数组 nums
, 找到含有相同数量的 0
和 1
的最长连续子数组,并返回该子数组的长度。
思路:
如果将所有的0都换为-1,那这个题找的就是和为0的最长子数组,这个题就与和为K的子数组相同了,不一样的是这个题要的是最长子数组的长度。
思路依旧是以i为终点,找i之前前缀和为sum[i]的下标计算长度,这里有个问题,如果之前存在前缀合为sum的下标,后面就不能直接更新了,因为肯定是以前那个下标到i的距离长,只有不存在sum时才会更新sum映射的下标
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;
}
};
四、矩阵区域和
题目链接:
题目介绍:
给你一个 m x n
的矩阵 mat
和一个整数 k
,请你返回一个矩阵 answer
,其中每个 answer[i][j]
是所有满足下述条件的元素 mat[r][c]
的和:
i - k <= r <= i + k,
j - k <= c <= j + k
且(r, c)
在矩阵内。
m == mat.length
n == mat[i].length
1 <= m, n, k <= 100
1 <= mat[i][j] <= 100
思路:
这个题光看示例很难理解,我们以一个例子来讲这个题是要干什么
假设我们要求answer[0][0]的值,k为1.
其实就是求从(0,0)向四周扩展1格,然后求这个扩展后的矩形所有元素的和
与二维前缀和那个题目思路相同,先创建出一个二维前缀和数组,dp[i][j]表示以(0,0)为左上角,(i,j)为右下角的矩形的和。想要计算某个区域的面积,只需要提供这个区域两个角的坐标就可以了。
但是构建二维前缀和数组和使用是与之前那个题不一样的,因为那个题的二维前缀和数组的下标适合矩形的下标正好对应的,但是这个题为了方便解决边界情况,我们还需要将二维前缀和数据多加一行一列,但是这个题给的矩阵是从(0,0)开始的,所以我们定义的这个数组要映射到矩阵的话就要减1,同理,从矩阵映射到数组的话就要加1。
另外,因为我们要求左上角和右下角的坐标,但我们向外扩展时,坐标可能会到边界外,需要控制一下,因为要使用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>> answer(mat.size(),vector<int>(mat[0].size()));
vector<vector<int>> dp(mat.size()+1,vector<int>(mat[0].size()+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];
}
}
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;
int x2=min(m-1,i+k)+1;
int y2=min(n-1,j+k)+1;
answer[i][j]=dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1];
}
}
return answer;
}
};