【问题标题】:Algorithm: how can't I use sliding window approach for this question?算法:这个问题我怎么不能使用滑动窗口方法?
【发布时间】:2021-06-20 16:10:43
【问题描述】:

我在一次采访中遇到了这个问题。它为您提供了一个数组和一个阈值,该函数应返回该数组的最短、非空、连续子数组的长度,其总和至少超过该阈值。

所以如果数组是[2,-1,2] 并且阈值是3 那么它应该返回3

这是我用 JavaScript 编写的尝试。我采用了典型的滑动窗口方法,一旦总和大于threshold,我将减小窗口大小并在每次迭代期间跟踪窗口大小。

function(array, threshold) {
  let minWindowSize = Infinity
  let sum = 0
  for (let start = 0, end = 0; end < array.length; end++) {
    sum += array[end]
    if(sum >= threshold) minWindowSize = Math.min(minWindowSize, end - start + 1)
    while (sum > threshold) {
      sum -= array[start++]
    }
  }

  return minWindowSize === Infinity ? -1 : minWindowSize
};

但是对于像这样的情况,我的解决方案是错误的

array = [17,85,93,-45,-21]
threshold = 150

我想知道这个问题与典型的滑动窗口问题有什么区别,有没有办法实现滑动窗口方法来解决这个问题?看起来很简单,但结果却是be a hard question on leetcode

【问题讨论】:

    标签: javascript algorithm sliding-window


    【解决方案1】:

    正如 David 所指出的,当数组中有负数时,您不能使用滑动/拉伸窗口技术,因为总和不会随窗口大小单调增长。

    不过,您仍然可以在 O(n log n) 时间内解决这个问题,使用通常用于“滑动窗口最小/最大值”问题的技术。

    首先,通过将每个元素替换为该元素和所有先前元素的总和,将您的数组转换为前缀和数组。现在你的问题变成了“找到差值 >= X 的最接近的元素对”(假设数组 [-1]==0)。

    当您遍历数组时,您需要为每个 i 找到最新的索引 j,这样 j 和array[j] .

    为了快速做到这一点,首先要注意 array[j]always 小于以下所有元素,直到 i ,因为否则会有更接近的元素可供选择。

    因此,当您遍历数组时,请维护一个堆栈,其中包含所有小于您所看到的所有后续元素的元素的索引。这很简单,总体上需要 O(n) 时间——在处理完每个 i 之后,您只需弹出所有具有 >= 值的索引,然后推送 i。 p>

    然后对于每一个i,你可以在这个栈中做一个二分查找,找到最新的具有足够小的值的索引。二进制搜索有效,因为堆栈中索引的值单调增加 - 每个元素必须小于以下所有元素。

    使用二分查找,to总时间增加到O(n log n)。

    在 JavaScript 中,它看起来像这样:

    var shortestSubarray = function(A, K) {
        //transform to prefix sum array
        let sum=0;
        const sums = A.map(val => {
            sum+=val;
            return sum;
        });
        const stack=[];
        let bestlen = -1;
        for(let i=0; i<A.length; ++i) {
            const targetVal = sums[i]-K;
            //binary search to find the shortest acceptable span
            //we will find the position in the stack *after* the
            //target value
            let minpos=0;
            let maxpos=stack.length;
            while(minpos < maxpos) {
                const testpos = Math.floor(minpos + (maxpos-minpos)/2);
                if (sums[stack[testpos]]<=targetVal) {
                    //value is acceptable.
                    minpos=testpos+1;
                } else {
                    //need a smaller value - go left
                    maxpos=testpos;
                }
            }
            if (minpos > 0) {
                //found a span
                const spanlen = i - stack[minpos-1];
                if (bestlen < 0 || spanlen < bestlen) {
                    bestlen = spanlen;
                }
            } else if (bestlen < 0 && targetVal>=0) {
                // the whole prefix is a valid span
                bestlen = i+1;
            }
            // Add i to the stack
            while(stack.length && sums[stack[stack.length-1]] >= sums[i]) {
                stack.pop();
            }
            stack.push(i);
        }
        return bestlen;
    };
    

    Leetcode 说:

    成功:

    运行时间:216 毫秒,比 JavaScript 在线提交的 100.00% 快 求和至少为 K 的最短子数组。

    内存使用:50.1 MB,不到在线 JavaScript 的 37.37% 提交总和至少为 K 的最短子数组。

    我猜大多数人使用的是较慢的算法。

    【讨论】:

    • 后来我注意到每个“迄今为止最好的”扫描的左索引单调增加,因此您可以摆脱二进制搜索并获得 O(N) 解决方案。似乎他们在这个问题的 leetcode 讨论中知道这一点,这让我想知道为什么所有其他 JS 解决方案都比这个慢。哦,好吧。
    【解决方案2】:

    如果所有数组元素都是非负数,您可以使用滑动窗口。问题是,对于负元素,一个子数组可能比另一个子数组短,并且总和大于另一个子数组。

    我不知道如何用滑动窗口解决这个问题。我想到的方法是循环前缀总和,在搜索段树以查找至少小于threshold 的最近总和后将每个插入到段树中。

    【讨论】:

    • 有趣。那么滑动窗口的方法只能用于不混有正负数的数组?
    • @Joji 基本上你需要能够通过增加一个端点来向下调整值,并通过增加另一个端点来向上调整值,所以是的,如果符号混合,情况并非如此。跨度>
    猜你喜欢
    • 2021-07-01
    • 2012-01-06
    • 2011-05-06
    • 1970-01-01
    • 2012-02-17
    • 2010-11-14
    • 2016-09-25
    • 2011-08-22
    • 1970-01-01
    相关资源
    最近更新 更多