【前缀和】和为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]-kpre[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;
    }
}
posted @ 2020-05-28 11:38  gzshan  阅读(1276)  评论(0编辑  收藏  举报