【算法】前缀和(下)
目录
一、和为k的子数组
二、和可被k整除的子数组
三、连续数组
四、矩阵区域和
一、和为k的子数组
题目链接:560. 和为 K 的子数组 - 力扣(LeetCode)
题目描述:
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2输出:2
示例 2:
输入:nums = [1,2,3], k = 3输出:2
提示:
- 1 <= nums.length <= 2 * 10^4
- -1000 <= nums[i] <= 1000
- -10^7 <= k <= 10^7
题目分析:
其实就是在数组nums中,找出 和为k 的子数组并统计个数。注意:子数组是连续非空的
解题思路:
设i为数组中的任意位置,使用sum[i]表示[0,i]区间内的所有元素之和
我们想知道有多少个【以i为结尾的 和为k的子数组】,则需要在[0,i-1]区间内找到 起始位置x1,x2,...;使得[x,i]区间内的元素和为k。那么[0,x-1]区间内的元素之和,便等于sum[i]-k
于是问题就转变为:
·在区间[0,i-1]内,找到有几个 前缀和 等于sum[i]-k 的即可
注意:本题我们只关心在i位置之前,有多少个 前缀和 等于sum[i]-k。所以不需要额外初始化一个前缀和数组,我们直接使用哈希表,一边求当前位置的 前缀和,一边存下 上一位置 前缀和出现的次数
注意:当sum[i]-k=0时,则表示sum[i]此时等于k。这便表示在[0,i]区间内,有一个起始位置满足了前缀和等于sum[i]-k。所以我们需要在哈希表中,初始化key=0时,value=1。
如果nums=[1],k=1时,上述初始化能够保证算出一个符合sum[i]-k的起始位置
简单来说,就是记录下[0,i]区间内的前缀和,当在[x,i]区间内的元素和为k时,然后在[0,x-1]区间内找到前缀和 为sum[i]-k的起始位置即可。
在代码中的话,我们应该在计算完前缀和后,判断哈希表中key=sum[i]-k的value值,如果非0,则统计下来。之后,哈希表再进行记录前缀和
解题代码:
Public static int subarraySum(int[] nums, int k) {
Map<Integer,Integer> hash=new HashMap<>();
hash.put(0,1);//初始化,当sum[i]-k=0时,有一个起始位置满足情况
int sum=0;
int ret=0;
for(int x:nums){
sum+=x;//计算当前位置的前缀和
//只要当key=sum-k时,value不为0,那么记录下此时有几个起始位置
ret+=hash.getOrDefault(sum-k,0);//记录有多少个起始位置
hash.put(sum,hash.getOrDefault(sum,0)+1);//把当前位置的 前缀和 放在哈希表中
}
return ret;
}
二、和可被k整除的子数组
题目链接:974. 和可被 K 整除的子数组 - 力扣(LeetCode)
题目描述:
给定一个整数数组
nums
和一个整数k
,返回其中元素之和可被k
整除的非空 子数组 的数目。子数组 是数组中 连续 的部分。
示例 1:
输入:nums = [4,5,0,-2,-3,1], k = 5 输出:7 解释: 有 7 个子数组满足其元素之和可被 k = 5 整除: [4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]示例 2:
输入: nums = [5], k = 9 输出: 0提示:
1 <= nums.length <= 3 * 10^4
-10^4 <= nums[i] <= 10^4
2 <= k <= 10^4
题目分析:
其实本题就是找到 元素和可以整除k的子数组的个数
解题思路和上一次类似
在解决本题之前,我们需要了解以下知识
同余定理:
如果(a-b)%n==0,那么我们可以得到一个结论:a%n==b%n。用文字叙述就是,如果两个数相减的差能被n整除,那么这两个数对n取模的结果相同
例如:(26-2)%2==0,那么26%12==2%12==2
c++中负数取模的结果,以及如何修正【负数取模】的结果
·c++中关于负数的取模运算,结果是【把负数当成整数,取模之后的结果加上一个负号】
如:-1%3= -(1%3)= -1
·因为有负数,为了防止发生【出现负数】的结果,以(a%n+n)%n的形式输出保证为正
如: -1%3=( -1%3+3)%3=2
解题思路:
设i为数组中的任意位置,用sum[i]表示区间[0,i]内所有元素的和
我们想知道有多少个【以i为结尾的 可被k整除的子数组】,就需要找到有多少个起始位置为x1,x2...使得[x,i]区间内的所有元素和 可被k整除
设[0,x-1]区间内所有元素之和为a,[0,i]区间内所有元素之和为b,那么(b-a)%k==0
由同余定理可得,[0,x-1]区间与[0,i]区间内的前缀和同余。
于是问题就转变为:在[0,x-1]区间内,找到有多少 前缀和的余数 等于 sum[i]%k 即可
简单来说,就是记录下[0,i]区间内的前缀和,然后在[0,x-1]区间内找到前缀和的余数 等于sum[i]%k即可。
在代码中的话,我们应该在计算完前缀和后,判断哈希表中key=(a%n+n)%n的value值,如果非0,则统计下来。之后,哈希表再进行记录前缀和
解题代码:
class Solution {
public static int subarraysDivByK(int[] nums, int k) {
int n = nums.length;
int[] s = new int[n];
s[0] = nums[0];
for (int i = 1; i < n; i++) {
s[i] = s[i - 1] + nums[i];
}
int res=0;
Map<Integer,Integer> cnt=new HashMap<>();
for(int x:s){
int t=(x%k+k)%k;
if(t==0) res++;
res+=cnt.getOrDefault(t,0);
cnt.put(t,cnt.getOrDefault(t,0)+1);
}
return res;
}
}
三、连续数组
题目链接:525. 连续数组 - 力扣(LeetCode)
题目描述:
给定一个二进制数组
nums
, 找到含有相同数量的0
和1
的最长连续子数组,并返回该子数组的长度。示例 1:
输入:nums = [0,1] 输出:2 说明:[0, 1] 是具有相同数量 0 和 1 的最长连续子数组。示例 2:
输入:nums = [0,1,0] 输出:2 说明:[0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。示例 3:
输入:nums = [0,1,1,1,1,1,0,0,0] 输出:6 解释:[1,1,1,0,0,0] 是具有相同数量 0 和 1 的最长连续子数组。提示:
1 <= nums.length <= 10^5
nums[i]
不是0
就是1
注意:题目已告知nums[i]不是0就是1
我们这里同样将问题进行转变。我们将元素为0的改为-1,元素为1的不变。
这样,我们只需要找到 和为0的最长连续子数组
这样,本题与 和为k的子数组 的解题思路就一致了
解题思路:
设i为数组中的任意位置,用sum[i]表示[0,i]区间内所有元素的和
我们想知道最大的【以i为结尾的 和为0的子数组】,就要找到从左往右第一个起始位置x1,使得[x1,i]区间内的所有元素之和为0.那么[0,x1-1]区间内的元素之和 就是sum[i]了。
于是问题就转变为:在[0,x-1]内,找到第一次出现sum[i]的位置即可
我们这里同样使用哈希表,一边计算前缀和,一边判断此时的key=sum是否存在哈希表中,如果存在的话,则计算与此处的距离;如果不存在,则将sum存入哈希表中
初始化key=0,value=-1的原因
如下图:
当出现第一次sum=0时,如果我们不初始化key=0的情况,我们就不能进入判断语句,进行计算数组长度。
当初始化key=0时,value值的设置又成了一大问题。
在计算数组长度时,我们用下标相减来计算。
由于下标从0开始,所以我们需要在下标相减后进行+1
所以,我们可以将value值设置为-1。这样下标相减时,-(-1)就变成了下标+1
将key=0,value=-1存入哈希表,还能解决整个数组和为0的情况。即:最后一个元素下标+1
解题代码:
class Solution {
public static int findMaxLength(int[] nums) {
Map<Integer,Integer> hash=new HashMap<>();
hash.put(0,-1);//默认一个前缀和为0的情况
int sum=0;
int ret=0;
for(int i=0;i<nums.length;i++){
sum+=(nums[i]==0?-1:1);
if(hash.containsKey(sum)){//判断在[0,x-1]区间内是否存在 前缀和为sum[i]的
ret=Math.max(ret,i-hash.get(sum));
}else hash.put(sum,i);//只记录第一次出现的sum
}
return ret;
}
}
四、矩阵区域和
题目链接:https://leetcode.cn/problems/matrix-block-sum/description/
题目描述:
给你一个
m x n
的矩阵mat
和一个整数k
,请你返回一个矩阵answer
,其中每个answer[i][j]
是所有满足下述条件的元素mat[r][c]
的和:
i - k <= r <= i + k,
j - k <= c <= j + k
且(r, c)
在矩阵内。示例 1:
输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 1 输出:[[12,21,16],[27,45,33],[24,39,28]]示例 2:
输入:mat = [[1,2,3],[4,5,6],[7,8,9]], k = 2 输出:[[45,45,45],[45,45,45],[45,45,45]]提示:
m == mat.length
n == mat[i].length
1 <= m, n, k <= 100
1 <= mat[i][j] <= 100
题目分析:
根据题目中:
i - k <= r <= i + k,
j - k <= c <= j + k
我们可以知道矩阵的行与列的区间。也就是矩阵的左上角和右下角。然后根据【前缀和(上)】的二维前缀和的公式即可解题
解题思路:
二维前缀和的简单应用题,关键就是我们在填写结果矩阵的时候,要找到原矩阵对应区域的【左上角】以及【右下角】的坐标--这里推荐大家画图
左上角坐标:x1=i-k,y1=j-k,但是这里可能会【超过矩阵】的范围,因此需要对0取一个max。因此修正后的坐标为:x1=max(0,i-k),y1=max(0,j-k)
右下角坐标:x2=i+k,y2=j+k,这里仍然可能会【超过矩阵】的范围,因此需要对m-1,以及n-1去一个min。因此修正后的坐标为:x2=min(m-1,i+k),y2=min(n-1,j+k)
然后将求出来的坐标代入到【二维前缀和矩阵】的计算公式上即可
解题代码:
public static int[][] matrixBlockSum(int[][] mat, int k) {
int m=mat.length;
int n=mat[0].length;
//前缀和矩阵
int[][] sum=new int[m+1][n+1];
//处理前缀和矩阵
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
sum[i][j]=sum[i-1][j]+sum[i][j-1]
-sum[i-1][j-1]+mat[i-1][j-1];
}
}
int[][] answer=new int[m][n];
//计算下标--我们确定好矩阵的左上角和右上角即可
//即i和j的最小值和最大值
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
//注意:需要判断左上角和右上角的下标是否越界
//我们这里通过max和min让其满足不会越界
int x1=Math.max(0,i-k)+1;//前缀和矩阵多加一行和一列的0,所以需要+1
int y1=Math.max(0,j-k)+1;
int x2=Math.min(m-1,k+i)+1;
int y2=Math.min(n-1,k+j)+1;
answer[i][j]=sum[x2][y2]-sum[x1-1][y2]-sum[x2][y1-1]+sum[x1-1][y1-1];
}
}
return answer;
}