zoukankan      html  css  js  c++  java
  • LeetCode“前缀和”系列(1)——第974题详解

    一、什么是“前缀和”?

    前缀和就是数组第0项到当前项的和。如果用一个数组preSum表示数组A的前缀和:

    preSum[i]=A[0]+A[1]+...+A[i];

    数组A中的某项,可以表示为相邻前缀和之差:

    A[i]=preSum[i]-preSum[i-1]

    多项叠加,等号右边加减相消,得到通式:

    A[i]+…+A[j]=preSum[j]−preSum[i−1]

    i 当然可以为 0,此时 i - 1 为 - 1,我们故意让 preSum[-1] 为 0,此时:

    A[0]+A[1]+…+A[j]=preSum[j]

    预置这种不存在的情况,只是为了让 preSum[0]preSum[0] 能套用通式。

    二、问题描述

    给定一个整数数组 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]

    三、问题等价转换

    子数组的元素之和=>数组第i项到第j项的和

    元素和能被K整除的子数组数目=>有几种i,j组合,使得A[i]到A[j]之和mod K==0

    转化为:

    • 有几种 i、j组合,满足 (preSum[ j ] - preSum[ i - 1 ])mod K== 0
    • 有几种 i、j组合,满足 preSum[j] mod K == preSum[i-1] mod K
    • 前提:preSum[j]preSum[j] 、preSum[i-1]preSum[i−1] 为正整数。负数的情况要处理

    前缀和数组 preSum 的项怎么求?

    • 当前项的前缀和 = 上一项的前缀和 + 当前项
    • 求出的 preSum 数组项,让它 mod K,mod 完再看哪两项相等,计数
    • 通式有 i、j两个变量,找出数组中所有相等的两项,需要 2 层循环去遍历 i,j
    • 时间复杂度:O(n^2) 。 我们希望继续优化

    我们只关心什么:数值和频次,降低时间复杂度

    • 前缀和与数组A的项一一对应,但我们不关心它具体对应到哪一项
    • 我们只关心出现过哪些 【前缀和 mod K】 数值,和对应的出现次数
    • 用一个变量 preSumModK ,保存每次求出的 前缀和 mod K,存入哈希表
    • 存键值对:
      • key:前缀和 mod K 。数值 作为 key
      • value:这个结果值出现了几次
    • 前缀和 mod K 的值正好是 0,1,2...,K-1,恰似索引,所以也可以用数组存,代码见最后

    找到 preSumModK 的递推关系,用于迭代计算

    • mod 的分配率: (a + b) mod c = (a mod c + b mod c) mod c
    • 当前的 preSumModK

        = (当前的前缀和) mod K
        = (上一项的前缀和 + A[i]) mod K
        = ((上一项的前缀和) mod K + A[i] mod K ) mod K
        = (上一个 preSumModK + A[i] mod K ) mod K
        = (上一个 preSumModK + A[i] ) mod K

    • 一前一后的 preSumModK 有了联系,便可在迭代中计算它

    整个流程过一遍

    • 预置 preSum[-1] = 0
      • -1 代表数组 A 的第 -1 项,即遍历数组 A 之前,map 提前放入 0:1,表示 求第 0 项前缀和之前,前缀和 mod K 等于 0 已经出现了 1 次
      • 这是违背现实的,但别纠结,只是为了求出第一个 preSumModK 而已
    • 遍历数组 A 的每一项,求当前项的 preSumModK ,存入 map 中
      • 之前没有存过,则作为 key 存入,值为 1
      • 之前存过,则对应值 +1
      • 于是 map 就录入了各项对应的【前缀和 mod K】
    • 边存 边查看已有的 key ,如果 map 中存在 key 等于 当前 preSumModK
      • 说明存在 之前求过的 preSumModK ,等于 当前 preSumModK
      • 把 key 对应的出现次数,累加给 count
      • 过去的某个前缀和,与当前前缀和搭配,差分出一个子数组
      • 出现过几次 ,就是有几个过去的前缀和,与当前前缀和,差分出几个满足条件的子数组

    尝试一句话概括

    • 根据 当前前缀和 mod K,在哈希表中找到与之 相等 的 key 。满足条件的 历史preSumModK 出现过 n 次,就是当前 前缀和 能找到 n 个历史前缀和,与它组合搭配,形成 n 个不同的子数组,满足元素和能被 K 整除
    • 遍历数组 A 每一项,重复以上步骤, n 不断累加给 count,最后返回 count

    算法复杂度

    • Time:O(n)
    • Space:O(K)。 mod 的结果最多 K 种,哈希表最多存放 K 个键值对

    补充:前缀和 为负数 的情况

    • 举例:K = 4,求得一个前缀和为 -1 , -1 % K = -1 ,3 % K = 3
    • 看似模的结果不相等,一个为 -1 一个为 3 ,但它们应该记到一组
    • 因为它们前缀和之差:3 - (-1) 为 4 。 4 % K = 0
    • 所以 mod K 的结果 -1 ,要加上 K ,转成正数的 3

    代码实现

    1、暴力法

        // 暴力方法,最后超时间限制
        public static int subarraysDivByK1(int[] A, int K) {
            int ans=0;
            for (int i = 0; i < A.length; i++) {
                int sum=0;
                for (int j = i; j < A.length; j++) {
                    sum+=A[j];
                    if(sum%K==0) ans++;
                }
            }
    
            return ans;
        }

    2、使用哈希表的代码

    // 前缀法
        public static int subarraysDivByK2(int[] A, int K) {
            int ans=0;
            Map<Integer,Integer> map=new HashMap<>();
            map.put(0,1);
            int preSumModK=0;
    
            for (int i = 0; i < A.length; i++) {
                preSumModK=(preSumModK+A[i])%K;
                if(preSumModK<0) preSumModK+=K;
    
                if(map.containsKey(preSumModK)){
                    int temp=map.get(preSumModK);
                    ans+=temp;
                    map.put(preSumModK,temp+1);
                }
                else {
                    map.put(preSumModK,1);
                }
            }
            return ans;
        }

    3、使用数组

    // 前缀法,数组代替哈希表
        public static int subarraysDivByK3(int[] A, int K) {
            int ans=0;
            int []map=new int[K];
            map[0]=1;
            int preSumModK=0;
    
            for (int value : A) {
                preSumModK = (preSumModK + value) % K;
                if (preSumModK < 0) preSumModK += K;
    
                int temp = map[preSumModK];
                if (temp != 0) {
                    ans += temp;
                    map[preSumModK] = temp + 1;
                } else {
                    map[preSumModK] = 1;
                }
            }
    
            return ans;
        }

    参考资料:
    链接:https://leetcode-cn.com/problems/subarray-sums-divisible-by-k/solution/you-jian-qian-zhui-he-na-jiu-zai-ci-dai-ni-da-tong/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    Java实现 蓝桥杯 历届试题 连号区间数
    Java实现 蓝桥杯 历届试题 连号区间数
    Java实现 蓝桥杯 历届试题 连号区间数
    Java实现 蓝桥杯 历届试题 连号区间数
    Java实现 蓝桥杯 历届试题 连号区间数
    Java实现 蓝桥杯 历届试题 大臣的旅费
    Java实现 蓝桥杯 历届试题 大臣的旅费
    Java实现 蓝桥杯 历届试题 大臣的旅费
    Java实现 蓝桥杯 历届试题 大臣的旅费
    Navicat查询哪些表有指定字段名
  • 原文地址:https://www.cnblogs.com/SupremeBoy/p/12976983.html
Copyright © 2011-2022 走看看