{"id":"dsa-complexity-sliding-window","slug":"sliding-window","title":"Challenge: sliding windows","text":"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.\n\n### A window of fixed size\n\nThe 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:\n\n```python\ndef best_sum(values, k):\n    total = sum(values[:k])\n    best = total\n    for i in range(k, len(values)):\n        total += values[i] - values[i - k]\n        best = max(best, total)\n    return best\n\n\nprint(best_sum([4, 2, 12, 3, 8, 1, 7], 3))\n```\n\n```output\n23\n```\n\nThe 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.\n\n### A window that grows and shrinks\n\nSometimes 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:\n\n```python\ndef longest_within(values, limit):\n    left = 0\n    total = 0\n    best = 0\n    for right in range(len(values)):\n        total += values[right]\n        while total > limit:\n            total -= values[left]\n            left += 1\n        best = max(best, right - left + 1)\n    return best\n\n\nprint(longest_within([2, 1, 3, 2, 6, 1, 1, 1, 2], 6))\nprint(longest_within([5, -4, 1], 2))  # a negative value: see below\n```\n\n```output\n4\n2\n```\n\nThere'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.\n\n**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`.\n\nThe 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.\n\n### Three jobs in one loop\n\nLook again at the loop of `longest_within`. Each round does three jobs, and most windows of changing size do the same three:\n\n- **Grow:** the `for` loop hands `right` the next value, and `total += values[right]` takes it in.\n- **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).\n- **Measure:** now the window from `left` to `right` is allowed, and `right - left + 1` may be the best so far.\n\nOnly 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.\n","task":"`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):\n\n```\n4\n```\n","kind":"fill","seed":"def count_windows(values, k, goal):\n    total = sum(values[:k])\n    count = 1 if total >= goal else 0\n    for i in range(k, len(values)):\n        leaving = values[i - k]\n        [[total += values[i] - leaving‖total = total + values[i] - leaving]]\n        if total >= goal:\n            count += 1\n    return count\n\n\nif __name__ == \"__main__\":\n    print(count_windows([4, 2, 12, 3, 8, 1, 7], 3, 16))\n","solution":"def count_windows(values, k, goal):\n    total = sum(values[:k])\n    count = 1 if total >= goal else 0\n    for i in range(k, len(values)):\n        leaving = values[i - k]\n        total += values[i] - leaving\n        if total >= goal:\n            count += 1\n    return count\n\n\nif __name__ == \"__main__\":\n    print(count_windows([4, 2, 12, 3, 8, 1, 7], 3, 16))\n","hints":["The window moves on by one: `values[i]` enters it, and `leaving`, the value k places back, leaves it. Summing the whole window again would be O(k) per move.","Add the value that enters and subtract the one that leaves, in one line: `total += values[i] - leaving`."],"tests":[{"name":"print(count_windows([4, 2, 12, 3, 8, 1, 7], 3, 16))","stdin":"","call":"print(count_windows([4, 2, 12, 3, 8, 1, 7], 3, 16))\n","expect":"4","hidden":false},{"name":"count_windows([5, 5, 5, 5], 2, 11)","stdin":"","call":"count_windows([5, 5, 5, 5], 2, 11)\n","expect":"0","hidden":true,"returns":true},{"name":"count_windows([1, 9, 1, 9, 1], 2, 10)","stdin":"","call":"count_windows([1, 9, 1, 9, 1], 2, 10)\n","expect":"4","hidden":true,"returns":true},{"name":"count_windows([3, 8], 2, 11)","stdin":"","call":"count_windows([3, 8], 2, 11)\n","expect":"1","hidden":true,"returns":true},{"name":"count_windows([i * 7 % 101 for i in range(300_000)], 150_000, 7_500_000)","stdin":"","call":"count_windows([i * 7 % 101 for i in range(300_000)], 150_000, 7_500_000)\n","expect":"75746","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[],"more":[{"task":"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.\n\nFor example, `longest_unique(\"abcabcbb\")` returns (`\"abc\"`):\n\n```\n3\n```\n","kind":"code","seed":"def longest_unique(text):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(longest_unique(\"abcabcbb\"))\n","solution":"def longest_unique(text):\n    last = {}\n    left = 0\n    best = 0\n    for right, char in enumerate(text):\n        if char in last and last[char] >= left:\n            left = last[char] + 1\n        last[char] = right\n        best = max(best, right - left + 1)\n    return best\n\n\nif __name__ == \"__main__\":\n    print(longest_unique(\"abcabcbb\"))\n","hints":["Move `right` along the text. Keep a dict `last` that maps each character to the index where it was last seen.","If the character at `right` was last seen inside the window, at an index of `left` or more, the window must start just after that copy: `left = last[char] + 1`. A copy before `left` is no longer in the window, so it doesn't count.","Then store the character's new index and compare the window's length, `right - left + 1`, with the best so far. Try `\"abba\"`: the answer is 2, and `left` must never move back."],"tests":[{"name":"longest_unique(\"abcabcbb\")","stdin":"","call":"longest_unique(\"abcabcbb\")\n","expect":"3","hidden":false,"returns":true},{"name":"longest_unique(\"\")","stdin":"","call":"longest_unique(\"\")\n","expect":"0","hidden":true,"returns":true},{"name":"longest_unique(\"aaaa\")","stdin":"","call":"longest_unique(\"aaaa\")\n","expect":"1","hidden":true,"returns":true},{"name":"longest_unique(\"abba\")","stdin":"","call":"longest_unique(\"abba\")\n","expect":"2","hidden":true,"returns":true},{"name":"longest_unique(\"pwwkew\")","stdin":"","call":"longest_unique(\"pwwkew\")\n","expect":"3","hidden":true,"returns":true},{"name":"longest_unique(\"dvdf\")","stdin":"","call":"longest_unique(\"dvdf\")\n","expect":"3","hidden":true,"returns":true},{"name":"longest_unique(\"\".join(chr(19968 + i % 20000) for i in range(200_000)))","stdin":"","call":"longest_unique(\"\".join(chr(19968 + i % 20000) for i in range(200_000)))\n","expect":"20000","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[]},{"task":"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).\n\nFor example, `shortest_stretch([2, 3, 1, 2, 4, 3], 7)` returns (4 and 3):\n\n```\n2\n```\n","kind":"code","seed":"def shortest_stretch(values, target):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(shortest_stretch([2, 3, 1, 2, 4, 3], 7))\n","solution":"def shortest_stretch(values, target):\n    left = 0\n    total = 0\n    best = 0\n    for right in range(len(values)):\n        total += values[right]\n        while total >= target:\n            if best == 0 or right - left + 1 < best:\n                best = right - left + 1\n            total -= values[left]\n            left += 1\n    return best\n\n\nif __name__ == \"__main__\":\n    print(shortest_stretch([2, 3, 1, 2, 4, 3], 7))\n","hints":["Grow the window with `right`, adding each value to `total`, as in `longest_within`.","While the total reaches the target, the window from `left` to `right` is a candidate: compare its length with the best so far, then take `values[left]` out and move `left` on to try a shorter one.","Use 0 for \"no window found yet\", and only replace the best when it's 0 or the new length is smaller."],"tests":[{"name":"shortest_stretch([2, 3, 1, 2, 4, 3], 7)","stdin":"","call":"shortest_stretch([2, 3, 1, 2, 4, 3], 7)\n","expect":"2","hidden":false,"returns":true},{"name":"shortest_stretch([1, 1, 1], 5)","stdin":"","call":"shortest_stretch([1, 1, 1], 5)\n","expect":"0","hidden":true,"returns":true},{"name":"shortest_stretch([5, 1, 9], 9)","stdin":"","call":"shortest_stretch([5, 1, 9], 9)\n","expect":"1","hidden":true,"returns":true},{"name":"shortest_stretch([1, 2, 3, 4, 5], 15)","stdin":"","call":"shortest_stretch([1, 2, 3, 4, 5], 15)\n","expect":"5","hidden":true,"returns":true},{"name":"shortest_stretch([], 3)","stdin":"","call":"shortest_stretch([], 3)\n","expect":"0","hidden":true,"returns":true},{"name":"shortest_stretch([1, 4, 4], 4)","stdin":"","call":"shortest_stretch([1, 4, 4], 4)\n","expect":"1","hidden":true,"returns":true},{"name":"shortest_stretch([i % 7 + 1 for i in range(200_000)], 200_000)","stdin":"","call":"shortest_stretch([i % 7 + 1 for i in range(200_000)], 200_000)\n","expect":"49999","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[]}]}