[20210602]LeetCode每日一题 - 523. 连续的子数组和

题目描述【中等】

给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:

子数组大小 至少为 2 ,且 子数组元素总和为 k 的倍数。 如果存在,返回 true ;否则,返回 false

如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 xk 的一个倍数。

示例 1:

输入:nums = [23,2,4,6,7], k = 6
输出:true
解释:[2,4] 是一个大小为 2 的子数组,并且和为 6

示例 2:

输入:nums = [23,2,6,4,7], k = 6
输出:true
解释:[23, 2, 6, 4, 7] 是大小为 5 的子数组,并且和为 42
426 的倍数,因为 42 = 7 * 67 是一个整数。

示例 3:

输入:nums = [23,2,6,4,7], k = 13
输出:false

提示:

1 <= nums.length <= 10^5
0 <= nums[i] <= 10^9
0 <= sum(nums[i]) <= 2^31 - 1
1 <= k <= 231 - 1

题目地址:地址


初步分析

本题涉及到一个算法:前缀和

  • 何为前缀和
    前缀和是为了快速求解类似于此类需要做 连续加法 的题而使用的数学工具。简单来说,就是一个新的、长度和原数组相同的数组 sumsum 的第 i 项是原数组 从 0 ~ i 的和。

  • 举个例子
    例:需要求得 [a, b] 区间范围的和,则可以用 sum[b] - sum[a - 1] 求得。如下:
    origin = [1, 2, 3, 4, 5]
    sum = [1, 3, 6, 10, 15]

    如果我要快速求得 origin 下标是 [2, 4] 之间的值的和,那我可以直接用 sum 数组的 sum[4] - sum[1] = 15 - 3 = 12 获得

  • 需要注意的是

    • 如果区间左侧下标为 0 时,注意不要越界(可以在初始化 sum 时,就预留第一位为 0 来避免越界,当然具体求解 [a, b] 区间时,也会变为 sum[b + 1] - sum[a]
    • 在实际解题的时候,可能不需要真的 new 出这个 sum 数组,而是用一个变量来累计循环数组的每项和而计算。就像今天的这道题一样。具体如何求解还是需要看题目的需求

如果需要做一个经典的题来练习这个算法,可以参考这个:560. 和为K的子数组


正式分析

我们继续使用上面分析使用的各项定义来具体分析此题

示例一 为例: nums = [23, 2, 4, 6, 7], k = 6
为了符合题目要求: 令整数 x 符合 x = n * k ,那么对于 sum 必然存在 (sum[b] - sum[a - 1]) % k === 0

sum[b]sum[a - 1]k 之间有什么关系呢:

sum[b] % k === sum[a - 1] % k

因为如果 [a, b] 之间存在一个整数 x,使得满足 x = n * k,那么必然在数组 nums[a - 1]nums[b] 之间的 可以被 k 整除,那也就是说 sum[a - 1]sum[b] 的差值,是符合条件的子集在被 k 取余时,导致余数不变,那么我们根据这个特点即可得出是否有子集满足此条件

因此我们需要创建一个数组,用来存储每次得到的余数,判断是否存在余数相同的情况

我们分析 示例一 的数据,如下:

  1. 23 % 6 = 5
  2. (5 + 2) % 6 = 1
  3. (1 + 4) % 6 = 5,此时我们发现此段余数与第一次的 余数相同,则代表着 [1, 2] 区间的值的和满足了条件

我们在观察上述过程,忽略了一个问题,就是假如 nums 中某值本身就符合条件,例如 示例二 的 nums = [23, 2, 6, 4, 7], k = 6 ,如果以上述方法计算,即:

  1. 23 % 6 = 5
  2. (5 + 2) % 6 = 1
  3. (1 + 6) % 6 = 1

时发现,在第三步就已经求得出余数相同的值了,而此时满足条件的下标为 [3, 3],实际上却不满足题目要求的另一个条件: 子数组大小 至少为 2。因此我们需要将每次取余后的值的 下标 记录并获取,然后判断 前一个取余后值得下标当前值下标 之间是否 >= 2 。所以我们可以使用 哈希表 来存储此对应关系。


具体代码

function checkSubarraySum(nums: number[], k: number): boolean {
    if (nums.length < 2) return false

    const map = new Map()
    map.set(0, -1)

    const length = nums.length
    let remain = 0

    for (let i = 0; i < length; i++) {
        remain = (remain + nums[i]) % k

        if (map.has(remain)) {
            const prevIndex = map.get(remain)

            if (i - prevIndex >= 2) return true

        } else {
            map.set(remain, i)
        }
    }

    return false
};

TS AC,执行用时:128ms, 在所有 TypeScript 提交中击败了 33.33% 的用户

TS AC中击败 100% 用户的提交,目前执行下来用时 180ms,估计力扣增加了测试用例导致更简单的算法耗时比之前提交的复杂的算法耗时要久,这对后期用户提交并不友好


问题升级

本题用到了一位数组的前缀和,那么自然也有二维数组前缀和
可以参考此题:304. 二维区域和检索 - 矩阵不可变

各大公司对前缀和的考察还是比较多的


最后

第一次看这道题一下反应出这是前缀和的题,但不是特别理解题目如何去解决。

跟着感觉写题解的时候,其实不是特别理解前缀和与余数在本题的作用,也不太理解照葫芦画瓢写的代码,还煞有其势地写了题解,发现越写越【顺】,以为就是这样去解题。所以第一次发布后,再看自己题解时发现了问题,竟然算错余数了 = =!

之后我再次重新勾勾画画后,才理解了前缀和与余数之间的微妙的关系,我愿称之为微妙,是因为这个解题法就跟脑筋急转弯一样,必须把示例中的值逐个计算,才能发现之间的特殊,很像小学数学奥赛题,写一写才能发现其特殊,然后才会有思路

所以以后还是不要偷懒在脑海中计算了,还是拿出纸画一画吧。。。

如果我在哪里写的有问题,欢迎指出,共同进步,谢谢阅读~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值