Session 2 · Sliding Window#
The big idea#
You are on a train and look through a window frame. You only see a few carriages at a time. When the train moves one step, one new carriage appears on the right and one disappears on the left. You don’t recount all the carriages you see; you just add the new one and remove the old one.
Now suppose a problem asks: “What is the longest stretch of a string with no repeated letter?” or “What is the biggest sum of 3 numbers in a row?” Checking every stretch from scratch is slow. A window that grows on the right and shrinks on the left does the job in one pass.
In plain words
Keep a window [left, right] over a sequence and a small state about what is inside (a sum,
a count, a set of letters). Grow it by moving right. If it breaks the rule, shrink it by moving left.
Reach for it when:
the problem is about a contiguous part: subarray or substring
you want the longest, shortest, biggest such part
there is a rule like “at most k distinct”, “no repeats”, “sum ≥ target”
The picture#
The right edge only ever moves forward, and so does the left edge. Together they move at most 2n
times, which is why the pattern is O(n) even though it has a loop inside a loop.
Two kinds of window#
Fixed size (window length k is given): slide by adding a[right] and removing a[right - k].
def max_sum_k(a, k):
window = sum(a[:k])
best = window
for right in range(k, len(a)):
window += a[right] - a[right - k] # one in, one out
best = max(best, window)
return best
print(max_sum_k([2, 1, 5, 1, 3, 2], 3))
Output:
9
(The best window is [5, 1, 3].)
Variable size (find the longest/shortest window that obeys a rule): grow, then shrink while broken.
def longest_unique(s):
seen = {} # letter -> how many times in the window
left = best = 0
for right, ch in enumerate(s):
seen[ch] = seen.get(ch, 0) + 1
while seen[ch] > 1: # rule broken: a repeat
seen[s[left]] -= 1
left += 1
best = max(best, right - left + 1)
return best
print(longest_unique("abcabcbb"))
Output:
3
Step by step#
s = "abcabcbb"
right |
char |
window |
repeat? |
action |
best |
|---|---|---|---|---|---|
0 |
a |
|
no |
grow |
1 |
1 |
b |
|
no |
grow |
2 |
2 |
c |
|
no |
grow |
3 |
3 |
a |
|
yes |
remove |
3 |
4 |
b |
|
yes |
remove |
3 |
5 |
c |
|
yes |
remove |
3 |
6 |
b |
|
yes |
remove |
3 |
7 |
b |
|
yes |
remove |
3 |
The recipe in four lines#
Move
rightforward and adda[right]to the state.While the window is invalid, remove
a[left]from the state and moveleftforward.Now the window is valid: record the answer (longest, or best sum).
Repeat until
rightreaches the end.
For the shortest window (for example “smallest subarray with sum ≥ target”), flip step 2 and 3: record the answer while the window is valid, and shrink as long as it still stays valid.
def min_subarray_len(target, a):
left = total = 0
best = float("inf")
for right, x in enumerate(a):
total += x
while total >= target: # still valid: try to make it smaller
best = min(best, right - left + 1)
total -= a[left]
left += 1
return 0 if best == float("inf") else best
print(min_subarray_len(7, [2, 3, 1, 2, 4, 3]))
Output:
2
Common mistakes#
Warning
Using it with negative numbers and a sum rule. Shrinking might increase the sum, so the logic breaks. Use prefix sums + a hash map (session 6).
Forgetting to remove
a[left]from the state before movingleft.Recording the answer before the window is valid again (for “longest” problems).
Off by one in the length: it is
right - left + 1.
Practice#
Problem |
Level |
What to notice |
|---|---|---|
Easy |
fixed window |
|
Medium |
the classic |
|
Medium |
shortest window |
|
Medium |
window valid if |
|
Medium |
fixed window of counts |
|
Hard |
shortest window + two counters |