【前缀和】和为K、和可被K整除的子数组
连续子数组问题是算法中经常可以见到的一类题目,通过几个典型的题目分析,可以发现这类题目主要分为两大类,其解题思路通过最简单的子串枚举(枚举所有的子串起点和终点)来暴力解决大都不难,但是如果考虑到对空间和时间的要求,其解答就需要一定的算法技巧。
- 子数组和问题(前缀和+哈希表)
- 子数组最值问题(多阶段决策过程最优化问题,动态规划)
【子数组】子数组和问题(前缀和)
一看到子数组和,有必要先对前缀和思想进行一些考虑。前缀和指的是:数组 第 0 项 到 当前项 的 总和,类似于我们在数学中学到的数列的前n项和。
如果用一个数组 pre[] 表示:pre[i]=nums[0]+nums[1]+···+nums[i]
前缀和的优势在于:数组中的某一项可以表示为相邻前缀和之差:nums[i]=pre[i]-pre[i-1]
,因此,从 i 到 j 范围子数组和就可以表示为:nums[i]+nums[i+1]+···+nums[j]=pre[j]-pre[i-1]
,这一关系就为求解子数组和问题提供了数学依据。
560、和为K的子数组
题目描述:给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例:
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
解题思路:
解法一:暴力枚举
暴力枚举子数组的起点 i 和终点 j,对其中的元素进行求和,得到和判断是否等于k,时间复杂度为O(n^3)
,但是对于子数组累积求和可以在遍历的过程中同步进行记录,因此,暴力法的时间复杂度最多可以优化到O(n^2)
解法二:前缀和+哈希表
正如前面提到的前缀和的优势,从 i 到 j 范围子数组和可以表示为:nums[i]+nums[i+1]+···+nums[j]=pre[j]-pre[i-1]
,那么从 i 到 j 范围子数组和为K这个条件可以表示为:pre[j]-pre[i-1]=k
.
因此,这就可以得到pre[i-1]=pre[j]-k
,所以考虑以 j 结尾的和为 k的连续子数组个数时,只要统计有多少个前缀和为pre[j]-k
的pre[i]
即可。通过建立哈希表,以前缀和作为键,该和出现的次数作为值,在遍历数组时,一边统计前缀和,一边从哈希表中得到对应的次数,从而得到最后的答案,具体可以参见代码实现。
代码实现:
//解法一:暴力枚举,时间复杂度O(n^2),空间复杂度O(1)
class Solution {
public int subarraySum(int[] nums, int k) {
int count=0;
for(int i=0;i<nums.length;i++){
int sum=0;
for(int j=i;j<nums.length;j++){ //从i到j这个子数组,累积求和
sum+=nums[j];
if(sum==k)
count++;
}
}
return count;
}
}
//解法二:前缀和+哈希表,时间复杂度O(n),空间复杂度O(n)
class Solution {
public int subarraySum(int[] nums, int k) {
if(nums==null || nums.length==0)
return 0;
Map<Integer,Integer> map=new HashMap<>(); //<前缀和,次数>
map.put(0,1); //注意这是必要的,这可以保证不漏掉只有一个元素的子数组
int count=0,pre=0;
for(int i=0;i<nums.length;i++){
pre+=nums[i];
if(map.containsKey(pre-k)) //找pre-k出现的次数,pre-k
count+=map.get(pre-k);
map.put(pre,map.getOrDefault(pre,0)+1);
}
return count;
}
}
974、和可被K整除的子数组
题目描述:给定一个整数数组 A
,返回其中元素之和可被 K
整除的(连续、非空)子数组的数目。
示例:
输入:A = [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]
解题思路:
可以看到本题和上一题基本相同,不同之处仅仅在于上一题为和为K,而这里是和可以被K整除。其基本解题思路类似,但是需要用到一个数学上的同余定理:给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余。
解法一:暴力枚举
和上题类似,枚举所有子数组,求和后判断是否能被K整除即可,时间复杂度同样为O(n^2)。
解法二:前缀和+哈希表
从 i 到 j 范围子数组和可以被K整除这个条件可以表示为:pre[j]-pre[i-1] mod K == 0
,根据我们提到的同余定理,只需要满足pre[j] mod K == pre[i-1] mod K
,就可以满足题意。
因此,可以考虑对数组进行遍历,在遍历同时统计答案。当我们遍历到第 i 个元素时,我们统计以 ji 结尾的符合条件的子数组个数。然后维护一个以前缀和模 K 的值为键,出现次数为值的哈希表 ,在遍历的同时进行更新。这样类似上一题进行维护和计算即可得到结果。
需要注意的是:不同的语言负数取模的值不一定相同,有的语言为负数,如Java 取模的特殊性,当被除数为负数时取模结果为负数,需要纠正。这是因为比如:k是2,序列是-3 4 9 ,那么模如果不是正数 ,会分别是 -1 1 0 ,而 -1 和 1之间刚好是距离k的,却不被统计,这就会漏掉子数组为2的这个答案,纠正的方法是:(pre%K+K)%K
代码实现:
class Solution {
public int subarraysDivByK(int[] A, int K) {
//哈希表+前缀和,时间复杂度O(n),空间复杂度O(n)
//两个数模K的结果相等, 其差能被K整除
if(A==null || A.length==0)
return 0;
Map<Integer,Integer> map=new HashMap<>(); //<前缀和,次数>
map.put(0,1);
int pre=0,res=0;
for(int i=0;i<A.length;i++){
pre+=A[i]; //求前缀和
int temp=(pre%K+K)%K; //注意 Java 取模的特殊性,当被除数为负数时取模结果为负数,需要纠正
if(map.containsKey(temp))
res+=map.get(temp);
map.put(temp,map.getOrDefault(temp,0)+1);
}
return res;
}
}