【问题标题】:Finding number of subarrays whose sum equals `k`查找总和等于“k”的子数组的数量
【发布时间】:2017-07-12 19:32:34
【问题描述】:

我们会得到一个整数数组和一个值k。我们需要找到总和等于k 的子数组的总数。

我在网上(在 Leetcode 上)找到了一些有趣的代码,如下:

public class Solution {
    public int subarraySum(int[] nums, int k) {
        int sum = 0, result = 0;
        Map<Integer, Integer> preSum = new HashMap<>();
        preSum.put(0, 1);

        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            if (preSum.containsKey(sum - k)) {
                result += preSum.get(sum - k);
            }
            preSum.put(sum, preSum.getOrDefault(sum, 0) + 1);
        }

        return result;
    }
}

为了理解它,我浏览了一些具体的例子,比如[1,1,1,1,1]k=3[1,2,3,0,3,2,6]k=6。虽然代码在这两种情况下都能完美运行,但我没有遵循它实际计算输出的方式。

我有两个具体的困惑:

1) 为什么代码会不断地在数组中添加值,而不会将其清零?例如[1,1,1,1,1]k=3,一旦sum=3,我们不需要将sum 重置为零吗?不重置sum 不会影响查找后面的子数组吗?

2) 当我们找到 sum k 的子数组时,我们不应该简单地做 result++ 吗?为什么我们要添加preSum.get(sum-k)

【问题讨论】:

    标签: java algorithm


    【解决方案1】:

    让我们先处理您的第一个困惑点:

    代码不断对数组求和并且不重置sum 的原因是因为我们将总和保存在preSum(以前的总和)中。然后,任何时候我们到达sum-k 是前一个总和的点(比如在索引i),我们知道总和之间 索引i 和我们当前的索引正好是@ 987654329@.

    例如,在下图中i=2,我们当前的索引等于4,我们可以看到,因为9,我们当前索引的总和,减去3,索引的总和i,是6,索引24(含)之间的总和是6

    考虑这一点的另一种方法是看到从数组中丢弃[1,2](在我们当前的索引4)给我们一个sum 6的子数组,原因与上述类似(参见图片了解详细信息)。

    使用这种思维方法,我们可以说我们想从数组的前面丢弃,直到剩下一个 sum k 的子数组。我们可以通过说,对于每个索引,“只丢弃 1,然后丢弃 1+2,然后丢弃 1+2+3,等等”(这些数字来自我们的示例),直到我们找到 sum k 的子数组(在我们的示例中为k=6)。

    这提供了一个完全有效的解决方案,但请注意,我们将在数组的每个索引处执行此操作,从而一遍又一遍地求和相同的数字。节省计算的一种方法是保存这些总和以备后用。更好的是,我们已经将这些相同的数字相加得到我们当前的sum,所以我们可以随时保存这个总数。

    要找到一个子数组,我们可以查看我们保存的总和,减去它们并测试我们剩下的是否是k。必须减去每个保存的总和有点烦人,所以我们可以使用commutativity of subtraction 来查看如果sum-x=k 为真,sum-k=x 也为真。这样我们就可以看到x 是否是一个保存的和,如果是,就知道我们找到了一个大小为k 的子数组。哈希映射使这种查找变得高效。

    现在你的第二个困惑点:

    大多数时候你是对的,在找到合适的子数组后,我们可以做result++preSum 中的值几乎总是1,因此result+=preSum.get(sum-k) 将等同于result+=1result++

    唯一不是这样的情况是,当 preSum.put 被调用之前已经到达的 sum 时。我们如何才能回到我们已经拥有的sum?唯一的方法是使用负数,抵消以前的数字,或者使用零,这根本不影响总和。

    基本上,当子数组的和等于 0 时,我们会回到之前的 sum。此类子数组的两个示例是 [2,-2] 或普通的 [0]。有了这样一个子数组,当我们找到一个后来的、与 sum k 相邻的子数组时,我们需要将多个 1 添加到 result,因为我们发现了多个新子数组,其中一个具有零和子数组 ( sum=k+0) 和一个没有它的 (sum=k)。

    这也是preSum.put 中出现+1 的原因。每次我们再次到达相同的sum 时,我们都会找到另一个 零和子数组。对于两个零和子数组,找到一个带有sum=k 的新相邻子数组实际上会给出 3 个子数组:新子数组 (sum=k)、新子数组加上第一个零和 (sum=k+0),以及两者都包含的原始子数组零和 (sum=k+0+0)。这个逻辑也适用于更多数量的零和子数组。

    【讨论】:

    • “基本上,当子数组的总和等于 0 时,我们会回到之前的总和。”。我没有得到上面的例子,你能用另一个例子解释一下吗?谢谢。
    • 如果子数组等于0,它对总和没有任何贡献,所以有或没有它的总和是一样的,给出两个选项来实现相同的总和。
    • 第二点的解释令人困惑。问题是为什么 + preSum.get(sum-k) 而不是 result++?
    • @Plain_Dude_Sleeping_Alone 有什么困惑?我们不能将result++ 与零和子数组一起使用,一个新条目可能会导致发现多个新子数组。例如考虑k=6[1,2,3,0,3](3 个子数组)与[1,2,3,0,3,3](5 个子数组)。当我们找到额外的 3 时,就会发现 2 个和为 6 的新子数组([3,3] 和 [0,3,3]); result++ 只会增加 1。
    • 嗯,现在清楚了。另一种情况是 [1,- 2,3] 和 [ 3,-4,1, 1,-2,3] 。其中 [3,-4,1, . . .] 产生 0 但 [ 3,-4,1, 1,-2,3] 也将采用,因为它的总和为 2。
    猜你喜欢
    • 1970-01-01
    • 2022-01-05
    • 2018-07-31
    • 2014-05-18
    • 2018-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多