Python Arena
Step 6 of 6

Challenge: sliding windows

A window is a stretch of values in a row, seen through a frame that slides along the list. Plenty of interview problems ask for the best window: the 7 days in a row with the most sales, the longest stretch of a text with no repeated letter. Working out every window of k values from scratch costs O(n·k), and when the windows can be any length, even growing a window from each start costs O(n²). A sliding window reuses the work: when the window moves, only what enters it and what leaves it changes.

A window of fixed size

The best sum of k values in a row. Instead of sum(values[i - k + 1:i + 1]) at every position, an O(k) sum each time, keep the window's total. When the window moves one place, add the value that enters and subtract the one that leaves:

def best_sum(values, k):
    total = sum(values[:k])
    best = total
    for i in range(k, len(values)):
        total += values[i] - values[i - k]
        best = max(best, total)
    return best


print(best_sum([4, 2, 12, 3, 8, 1, 7], 3))
Output
23

The window that ends at i holds values[i - k + 1] to values[i], so values[i] enters and values[i - k], the value k places back, leaves. Each move is O(1), so the whole function is O(n), whatever k is.

A window that grows and shrinks

Sometimes the size isn't fixed: the longest stretch of days whose spending adds up to at most a budget, limit. Move right along one value at a time and add the new value. While the window breaks the rule, here a total over the limit, take values[left] out and move left on. Then the window from left to right is fine, and its length is a candidate:

def longest_within(values, limit):
    left = 0
    total = 0
    best = 0
    for right in range(len(values)):
        total += values[right]
        while total > limit:
            total -= values[left]
            left += 1
        best = max(best, right - left + 1)
    return best


print(longest_within([2, 1, 3, 2, 6, 1, 1, 1, 2], 6))
print(longest_within([5, -4, 1], 2))  # a negative value: see below
Output
4
2

There's a loop inside a loop, and still the function is O(n): right moves n times, and left only moves forward, so it moves at most n times in all, however the moves are spread. Each value enters the window once and leaves it at most once. The memory is O(1): a few numbers, however long the list is.

Why it's right: the values aren't negative, so adding a value never makes the total smaller. Once the window from left is over the limit, every longer window from left is too, and left can move on for good. With negative values that's no longer true, and the window can miss the answer. That's the second call above: [5, -4, 1] adds up to 2, within the limit, so the answer is 3. But the window drops the 5 as soon as the total goes over 2, before the -4 can bring it back down, and it returns 2. For a stretch that adds up to exactly k, negative values are no problem: use the prefix sums and the dict of the last step. For the biggest sum of a stretch of any length, use the last step's biggest_stretch.

The window's state isn't always a total. For a stretch of text with no repeated character, the state is which characters are in the window, kept in a set or in a dict of where each character was last seen. When the new character is already in the window, left moves past its earlier copy. The dict has one entry for each different character of the text: O(n) memory at most, and at most 26 entries for a text of lowercase letters.

Three jobs in one loop

Look again at the loop of longest_within. Each round does three jobs, and most windows of changing size do the same three:

  • Grow: the for loop hands right the next value, and total += values[right] takes it in.
  • Repair: while total > limit, the window isn't allowed, so it gives back values[left] and left moves on. Since left never moves back, all the repairs together cost O(n).
  • Measure: now the window from left to right is allowed, and right - left + 1 may be the best so far.

Only the state and the rule change from problem to problem. When the question is the shortest window that reaches a target, repairing and measuring trade places: while the window still reaches the target, measure it first, then give back values[left] to see whether a shorter window reaches it too.

Your task

Challenge 1 of 3

count_windows(values, k, goal) counts the windows of k values in a row whose total is at least goal. Fill in the blank that updates the total when the window moves one place: the hidden test has 300,000 values and k = 150,000, so summing each window again is too slow. The call print(count_windows([4, 2, 12, 3, 8, 1, 7], 3, 16)) should print (the windows 18, 17, 23 and 16):

4

Challenge 2

Write longest_unique(text): the length of the longest stretch of text in which no character appears twice. An empty text gives 0. The hidden test has a text of 200,000 characters whose answer is 20,000, so starting over at every position is too slow: slide a window instead. A dict of where each character was last seen tells you in O(1) where the window must start, for O(n) in all.

For example, longest_unique("abcabcbb") returns ("abc"):

3

Challenge 3

Write shortest_stretch(values, target): the length of the shortest stretch of values in a row whose sum is at least target, or 0 if no stretch reaches it. All the values are positive. The hidden test has 200,000 values, and the answer is about 50,000 long, so trying every start is too slow: slide a window, for O(n).

For example, shortest_stretch([2, 3, 1, 2, 4, 3], 7) returns (4 and 3):

2