4

I encountered this question during an interview. It gives you an array and a threshold and the function should return the length of the shortest, non-empty, contiguous subarray of that array with sum at least past that threshold.

So if the array is [2,-1,2] and threshold is 3 then it should return 3 .

Here is my attempt written in JavaScript. I was taking a typical sliding window approach, where once the sum is bigger than threshold I will decrease the window size and I keep track of the window size during each iteration.

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
};

However my solution is buggy for cases like

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

I wanted to know what differentiates this question from a typical sliding window question and is there a way to implement a sliding window approach to solve this question? It seems pretty straightforward but it turned out to be a hard question on leetcode.

Joji
  • 4,703
  • 7
  • 41
  • 86

2 Answers2

2

As David indicates, you can't use the sliding/stretching window technique when there are negative numbers in the array, because the sum doesn't grow monotonically with the window size.

You can still solve this in O(n log n) time, though, using a technique that is usually used for the "sliding window minimum/maximum" problem.

First, transform your array into a prefix sum array, by replacing each element with the sum of that element and all the previous ones. Now your problem changes to "find the closest pair of elements with difference >= X" (assuming array[-1]==0).

As you iterate though the array, you need to find, for each i, the latest index j such that j < i and array[j] <= array[i]-x.

In order to do that quickly, first note that array[j] will always be less than all the following elements up to i, because otherwise there would be a closer element to choose.

So, as you iterate through the array, maintain a stack of the indexes of all elements that are smaller than all the later elements you've seen. This is easy and takes O(n) time overall -- after processing each i, you just pop all the indexes with >= values, and then push i.

Then for each i, you can do a binary search in this stack to find the latest index with a small enough value. The binary search works, because the values for index in the stack increase monotonically -- each element must be less than all the following ones.

With the binary search, to total time increases to O(n log n).

In JavaScript, it looks like this:

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 says:

SUCCESS:

Runtime: 216 ms, faster than 100.00% of JavaScript online submissions for Shortest Subarray with Sum at Least K.

Memory Usage: 50.1 MB, less than 37.37% of JavaScript online submissions for Shortest Subarray with Sum at Least K.

I guess most people used a slower algorithm.

Matt Timmermans
  • 53,709
  • 3
  • 46
  • 87
  • I noticed later that the left index of every 'best so far' scan increases monotonically, so you can get rid of the binary search and have an O(N) solution. Seems they know this in the leetcode discussion of this problem, which makes me wonder why all the other JS solutions are slower than this one. Oh well. – Matt Timmermans Mar 25 '21 at 14:14
1

You could use a sliding window if all of the array elements were nonnegative. The problem is that, with negative elements, it's possible for a subarray to be both shorter than and have a greater sum than another subarray.

I don't know how to solve this problem with a sliding window. The approach that I have in mind would be to loop over the prefix sums, inserting each into a segment tree after searching the segment tree for the most recent sum that was at least threshold smaller.

David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • interesting. so sliding window approach can only be used for arrays that are not mixed with both positive and negative numbers? – Joji Mar 24 '21 at 01:26
  • @Joji basically you need to be able to adjust the value down by incrementing one endpoint and up by incrementing the other, so yes, if the signs are mixed, that isn't the case. – David Eisenstat Mar 24 '21 at 01:33