Sliding Window Patterns — Comprehensive Guide
The sliding window turns an O(n²) scan over every subarray/substring into a single O(n) pass. The core idea: keep a contiguous window [left, right] over the array, extend right to include new elements, and shrink left only when the window breaks a constraint. Each element enters the window once and leaves at most once, so the whole scan is O(n) even though the window is constantly resizing.
This guide builds from the two window shapes (fixed and variable), then walks 13 LeetCode problems that plug into them.
From-Scratch Idea: What Makes a Problem Sliding Window
Take the brute force for “longest substring with at most 2 distinct characters”:
best = 0
for i in range(n):
for j in range(i, n):
if distinct(s[i:j+1]) <= 2:
best = max(best, j - i + 1)
O(n²) windows, each costing O(n) to check — O(n³) total. Now notice two things the brute force throws away every iteration:
- The window
[i, j+1]is the window[i, j]plus one character. Recomputingdistinctfrom scratch discards everything you learned one step ago. Maintain a running count map and each extension is O(1). - If
[i, j]is already invalid, every longer window starting atiis also invalid. So the inner loop should stop, not keep going — and more than that, when you advancei, the newjnever has to restart fromi. It can resume where it was.
That second point is the real theorem, and it needs one property to hold:
Shrinking a window can never turn a valid window invalid (equivalently: validity is inherited by sub-windows). Call this monotone validity.
With monotone validity, left never needs to move backwards. Both pointers only ever advance, so
across the whole run they take at most 2n steps total — O(n), no matter how much the window
resizes in between.
The two questions that identify it
- Is the answer a contiguous region? A subarray or substring, not a subsequence and not a pair. Non-contiguous → this is not your pattern.
- Is validity monotone? Does removing an element from a valid window keep it valid? “At most k distinct” — yes. “Sum ≥ target with all-positive numbers” — yes. “Sum ≥ target with negatives” — no, and §13 shows what to do instead.
Where it breaks, and the two escapes
| The constraint | Monotone? | What to do |
|---|---|---|
| “at most k …” | Yes | Plain window |
| “sum ≥ target”, all non-negative | Yes | Plain window (§5) |
| “sum ≥ target”, negatives present | No | Prefix sums + monotonic deque (§13) |
| “exactly k distinct” | No | atMost(k) − atMost(k−1) (§12) |
| “maximum in every window” | N/A — not a validity question | Monotonic deque (§8) |
Both escapes are worth knowing cold: they turn “sliding window does not apply” back into “sliding window applies twice” or “sliding window on a different axis.”
Master LeetCode Comparison Table
| LC # | Title | Diff | Shape | What the window tracks | Cost |
|---|---|---|---|---|---|
| 643 | Maximum Average Subarray I | Easy | Fixed size k |
running sum | O(n) / O(1) |
| 3 | Longest Substring Without Repeating | Medium | Variable, longest | last-seen index per char | O(n) / O(Σ) |
| 209 | Minimum Size Subarray Sum | Medium | Variable, shortest | running sum (all positive) | O(n) / O(1) |
| 438 | Find All Anagrams in a String | Medium | Fixed + freq map | character counts vs target | O(n) / O(Σ) |
| 567 | Permutation in String | Medium | Fixed + freq map | same as 438, boolean answer | O(n) / O(Σ) |
| 424 | Longest Repeating Char Replacement | Medium | Variable + violation count | windowLen − maxFreq ≤ k |
O(n) / O(Σ) |
| 1004 | Max Consecutive Ones III | Medium | Variable + violation count | number of zeros flipped | O(n) / O(1) |
| 239 | Sliding Window Maximum | Hard | Fixed + monotonic deque | indices in decreasing value order | O(n) / O(k) |
| 340 | Longest Substring with At Most K Distinct | Medium | Variable, longest | len(count) ≤ k |
O(n) / O(k) |
| 76 | Minimum Window Substring | Hard | Variable, shortest | have == need quota counter |
O(n) / O(Σ) |
| 992 | Subarrays with K Different Integers | Hard | Two windows | atMost(k) − atMost(k−1) |
O(n) / O(k) |
| 862 | Shortest Subarray with Sum ≥ K | Hard | Prefix sums + deque | monotone prefix candidates | O(n) / O(n) |
The Shape column is the one to internalise. Only four things ever appear there: fixed size, variable-longest, variable-shortest, and “not really a window — a deque over a derived array”.
Quick Navigation: “I need to…”
| I need to… | Window shape | Section |
|---|---|---|
| Best/sum of every window of fixed size k | Fixed | 3 |
| Max in each window of size k | Fixed + monotonic deque | 8 |
| Longest substring/subarray under a constraint | Variable (grow, shrink when invalid) | 4 |
| Shortest subarray meeting a target | Variable (shrink while still valid) | 5 |
| Count / match by character frequency | Variable + count map | 6 |
| Longest window with ≤ k exceptions | Variable + violation counter | 9 |
| Shortest window covering all of a target multiset | Variable + quota counter | 10 |
| Longest window with at most k distinct | Variable + count map | 11 |
| Count subarrays with exactly k distinct | Two atMost windows |
12 |
| Shortest subarray with sum ≥ k, negatives allowed | Prefix sums + deque | 13 |
Table of Contents
- The Core Idea: Why Windows Beat Nested Loops
- The Two Window Shapes
- Fixed-Size Window
- Variable Window — Longest
- Variable Window — Shortest
- Frequency Window — Anagrams & Permutations
- The Universal Variable-Window Template
- Problem 239 — Sliding Window Maximum
- Problem 424 — Longest Repeating Character Replacement
- Problem 76 — Minimum Window Substring
- Problem 340 — Longest Substring with At Most K Distinct Characters
- Counting Exactly K: the
atMostReduction - Problem 862 — Shortest Subarray with Sum at Least K
- Master Comparison Table
- How to Identify This Pattern
- Practice Order
- Pattern Recognition Cheat Sheet
1. The Core Idea: Why Windows Beat Nested Loops
The brute force for “best subarray of length k” recomputes each window from scratch:
nums = [1, 3, -1, -3, 5], k = 3
Brute force: for each start i, sum nums[i..i+k-1]
i=0: 1 + 3 + (-1) = 3 ← re-adds 3 and -1
i=1: 3 + (-1) + (-3) = -1 ← re-adds -1 and -3
i=2: (-1) + (-3) + 5 = 1
Every element is touched k times → O(n·k)
The window insight: consecutive windows overlap almost entirely. Sliding from [i..i+k-1] to [i+1..i+k] only removes one element and adds one:
window sum: 3
slide → add nums[3]=-3, drop nums[0]=1 → 3 - 1 + (-3) = -1
slide → add nums[4]=5, drop nums[1]=3 → -1 - 3 + 5 = 1
Each element is added once and removed once → O(n).
★ Insight ─────────────────────────────────────
- The window is a summary you maintain incrementally. Recomputing it from scratch each step throws away the overlap — that’s the whole cost you’re eliminating.
- Sliding window only works when the answer for a window can be updated in O(1) from the neighbor (add one, drop one). If updating needs a full rescan, you gain nothing — reach for a different structure (see §8’s deque).
─────────────────────────────────────────────────
2. The Two Window Shapes
FIXED window VARIABLE window
size k never changes size grows and shrinks
[■ ■ ■] · · · [■] · · · · ·
· [■ ■ ■] · · right always [■ ■] · · · · right always advances;
· · [■ ■ ■] · advances by 1; [■ ■ ■] · · · left advances ONLY when
· · · [■ ■ ■] left follows [■ ■] · · · · a constraint is violated
k behind
- Fixed:
leftandrightmove in lockstep, exactlykapart. Use when the problem names a size (“window of size k”, “every k consecutive”). - Variable:
rightmarches forward always;leftjumps in only to restore validity. Use when the problem asks for the longest / shortest window satisfying a condition.
Almost every “substring / subarray with property X” problem is a variable window. That’s the workhorse — master §7’s template and most of these problems collapse to filling in three blanks.
3. Fixed-Size Window
Problem (LC 643 style): maximum average of any contiguous subarray of length k.
def max_sum_fixed(nums, k):
window = sum(nums[:k]) # first window
best = window
for right in range(k, len(nums)):
window += nums[right] - nums[right - k] # add new, drop old
best = max(best, window)
return best
nums = [1, 12, -5, -6, 50, 3], k = 4
first window [1,12,-5,-6] sum = 2
right=4: +50 -1 → [12,-5,-6,50] sum = 51
right=5: +3 -12 → [-5,-6,50,3] sum = 42
best = 51
The nums[right] - nums[right - k] line is the entire trick: right - k is the element leaving on the left as right enters on the right.
4. Variable Window — Longest
Problem (LC 3): longest substring without repeating characters.
Grow right. The moment a duplicate appears, shrink left until the window is valid again.
def length_of_longest_substring(s):
seen = {} # char → last index seen
left = 0
best = 0
for right, ch in enumerate(s):
if ch in seen and seen[ch] >= left:
left = seen[ch] + 1 # jump left past the duplicate
seen[ch] = right
best = max(best, right - left + 1)
return best
s = "abcabcbb"
right=0 'a' window "a" best=1
right=1 'b' window "ab" best=2
right=2 'c' window "abc" best=3
right=3 'a' dup! left→1 window "bca" best=3
right=4 'b' dup! left→2 window "cab" best=3
right=5 'c' dup! left→3 window "abc" best=3
right=6 'b' dup! left→5 window "cb"
right=7 'b' dup! left→7 window "b"
best = 3 ("abc")
★ Insight ─────────────────────────────────────
- The guard
seen[ch] >= leftmatters: a duplicate outside the current window (to the left ofleft) is stale and must not dragleftbackward. Windows only ever grow from the left —leftis monotonic non-decreasing. - “Longest valid” windows shrink just enough to become valid, then immediately try to grow again. Contrast with “shortest” (§5), which shrinks as much as possible while staying valid.
─────────────────────────────────────────────────
5. Variable Window — Shortest
Problem (LC 209): smallest length of a contiguous subarray with sum >= target (positive nums).
Grow right to reach the target, then shrink left greedily while the sum stays >= target.
def min_subarray_len(target, nums):
left = 0
window = 0
best = float('inf')
for right, x in enumerate(nums):
window += x
while window >= target: # shrink while STILL valid
best = min(best, right - left + 1)
window -= nums[left]
left += 1
return best if best != float('inf') else 0
target = 7, nums = [2,3,1,2,4,3]
right=0 win=2
right=1 win=5
right=2 win=6
right=3 win=8 ≥7 → record len 4 [2,3,1,2], shrink win=6 left=1
right=4 win=10≥7 → record len 4 [3,1,2,4], shrink win=7 left=2
win=7 ≥7 → record len 3 [1,2,4], shrink win=6 left=3
right=5 win=9 ≥7 → record len 3 [2,4,3], shrink win=7 left=4
win=7 ≥7 → record len 2 [4,3], shrink win=3 left=5
best = 2 (subarray [4,3])
Why this needs non-negative numbers. Growing
rightmonotonically increases the sum, and shrinkingleftmonotonically decreases it — that monotonicity is what letsleftnever backtrack. With negatives, adding an element can decrease the sum, so the window is no longer monotonic and a plain sliding window breaks. For arrays with negatives you need prefix sum + monotonic deque, which is exactly what §13 (LC 862) below builds — read this section first, then go see how the assumption gets repaired.
6. Frequency Window — Anagrams & Permutations
Problem (LC 438): find all start indices of anagrams of p in s.
A fixed window of size len(p), but validity is checked by frequency match, not a scalar. Track how many characters currently have the correct count.
from collections import Counter
def find_anagrams(s, p):
if len(p) > len(s):
return []
need = Counter(p)
window = Counter()
k = len(p)
result = []
for right, ch in enumerate(s):
window[ch] += 1
if right >= k: # drop the element leaving on the left
left_ch = s[right - k]
window[left_ch] -= 1
if window[left_ch] == 0:
del window[left_ch]
if window == need: # exact frequency match
result.append(right - k + 1)
return result
s = "cbaebabacd", p = "abc" (need = {a:1,b:1,c:1}), k=3
index: c0 b1 a2 e3 b4 a5 b6 a7 c8 d9
right=2 window {c,b,a} == need → append 2-3+1 = 0
right=3 add e, drop c {b,a,e} ✗
right=4 add b, drop b {a,e,b} ✗
...
right=8 add c, drop a {b,a,c} == need → append 8-3+1 = 6
result = [0, 6]
Comparing two Counters each step is O(26) = O(1) for lowercase letters. LC 567 (permutation-in-string) is the same window — return True on first match instead of collecting all.
7. The Universal Variable-Window Template
Nearly every variable-window problem fits this skeleton. You fill in three blanks: what state the window tracks, when it’s invalid, and what you record.
def sliding_window(s):
state = {} # window contents (count map, sum, etc.)
left = 0
answer = 0
for right in range(len(s)):
# 1. EXPAND: add s[right] to state
add(state, s[right])
# 2. SHRINK: while window violates the constraint, remove s[left]
while is_invalid(state):
remove(state, s[left])
left += 1
# 3. RECORD: window [left..right] is now valid
answer = max(answer, right - left + 1)
return answer
| Blank | “Longest without repeats” | “Longest with ≤ k distinct” | “Min sum ≥ target” |
|---|---|---|---|
state |
char count map | char count map | running sum |
is_invalid |
any count > 1 | len(map) > k |
(invert: shrink while valid) |
record |
right-left+1 on each step |
right-left+1 on each step |
right-left+1 inside shrink loop |
★ Insight ─────────────────────────────────────
- Longest problems record after the shrink loop (window is maximal-valid). Shortest problems record inside the shrink loop (each shrink is a candidate minimum). Getting this placement wrong is the #1 sliding-window bug.
is_invalidshould test a condition that only becomes true by adding and only becomes false by removing. If your condition can flip both ways on an add, a monotonic window won’t work.─────────────────────────────────────────────────
8. Problem 239 — Sliding Window Maximum
Difficulty: Hard
Return the maximum of every fixed window of size
k.
Naively rescanning each window is O(n·k). A running max doesn’t work — when the current max leaves the window, you’d have to rescan to find the next one. The fix: a monotonic decreasing deque holding indices whose values could still become a window max.
from collections import deque
def max_sliding_window(nums, k):
dq = deque() # indices, values decreasing front→back
result = []
for right, x in enumerate(nums):
# drop indices that fell out of the window on the left
if dq and dq[0] <= right - k:
dq.popleft()
# drop smaller values from the back — they can never be the max
# while x is in the window
while dq and nums[dq[-1]] <= x:
dq.pop()
dq.append(right)
if right >= k - 1:
result.append(nums[dq[0]]) # front = window max
return result
nums = [1,3,-1,-3,5,3,6,7], k = 3
right=0 dq=[0] (val 1)
right=1 3≥1 pop 0, dq=[1] (val 3)
right=2 dq=[1,2] → max nums[1]=3
right=3 dq=[1,2,3] → max 3
right=4 5 pops 3,2,1; expire? dq=[4] → max 5
right=5 dq=[4,5] → max 5
right=6 6 pops 5,4; dq=[6] → max 6
right=7 dq=[6,7] → max 7
result = [3,3,5,5,6,7]
★ Insight ─────────────────────────────────────
- The deque is the window’s max — but stored so the runner-up is already waiting behind the front. When the max expires, the next-largest is instantly at the front. That’s why we can afford O(1) amortized instead of rescanning.
- Each index is pushed once and popped once, so despite the inner
while, the total work is O(n). This “monotonic deque” is the bridge between sliding window and the Stack & Queue guide.─────────────────────────────────────────────────
9. Problem 424 — Longest Repeating Character Replacement
Difficulty: Medium
Longest substring where you may replace at most
kcharacters so all chars become the same.
A window is valid when (window length) - (count of the most frequent char) <= k — i.e. the non-majority chars, which are what you’d replace, number at most k.
from collections import defaultdict
def character_replacement(s, k):
count = defaultdict(int)
left = 0
max_freq = 0 # highest single-char count seen in the window
best = 0
for right, ch in enumerate(s):
count[ch] += 1
max_freq = max(max_freq, count[ch])
# replacements needed = window size - most common char count
if (right - left + 1) - max_freq > k:
count[s[left]] -= 1
left += 1
best = max(best, right - left + 1)
return best
s = "AABABBA", k = 1
right=0 A count{A:1} maxf=1 win"A" best=1
right=1 A {A:2} maxf=2 win"AA" best=2
right=2 B {A:2,B:1} maxf=2 need 1 ok best=3 "AAB"
right=3 A {A:3,B:1} maxf=3 win"AABA" need 4-3=1 ok best=4
right=4 B {A:3,B:2} maxf=3 need 5-3=2>1 → shrink left→1 win"ABAB"
right=5 B {A:2,B:3} maxf=3 need 5-3=2>1 → shrink left→2 win"BABB"
right=6 A stale maxf=3 need 5-3=2>1 → shrink left→3 best stays 4
best = 4
The subtle bit:
max_freqis never decreased when the window shrinks. That looks like a bug but isn’t — the answerbestcan only grow when a larger window is valid, which requires amax_freqat least as high as the stale one. Keeping the stale value never produces a wrong-larger answer, and it keeps the loop O(n) instead of O(26n). This is a well-known, deliberate trick.
10. Problem 76 — Minimum Window Substring
Given strings
sandt, return the shortest substring ofscontaining every character oft, including duplicates.s = "ADOBECODEBANC",t = "ABC"→"BANC".
This is the hardest shape in the family, and it is hard for one specific reason: the validity test
is “the window covers a multiset”, and re-checking that from scratch is O(alphabet) per step.
The fix is a single integer, have, counting how many distinct characters have met their
required quota. The window is valid exactly when have == need, which is an O(1) test.
from collections import Counter
def min_window(s, t):
if not t or not s:
return ""
required = Counter(t)
need = len(required) # distinct chars that must hit quota
have = 0 # how many have hit it right now
window = {}
best_len, best_lo = float('inf'), 0
lo = 0
for hi, ch in enumerate(s):
window[ch] = window.get(ch, 0) + 1
if ch in required and window[ch] == required[ch]:
have += 1 # note: == not >=, so we count each char once
while have == need: # valid -> shrink to find the minimum
if hi - lo + 1 < best_len:
best_len, best_lo = hi - lo + 1, lo
left = s[lo]
window[left] -= 1
if left in required and window[left] < required[left]:
have -= 1 # dropped below quota: window just became invalid
lo += 1
return "" if best_len == float('inf') else s[best_lo:best_lo + best_len]
s = "ADOBECODEBANC", t = "ABC" -> required = {A:1, B:1, C:1}, need = 3
hi=0 A window{A:1} have=1
hi=1 D have=1
hi=2 O have=1
hi=3 B window{B:1} have=2
hi=4 E have=2
hi=5 C window{C:1} have=3 -> VALID, shrink:
"ADOBEC" len 6 -> best = (6, 0)
drop A: window{A:0} < 1 -> have=2, lo=1
hi=6 O have=2
hi=7 D have=2
hi=8 E have=2
hi=9 B window{B:2} == required? no (2 != 1) -> have stays 2
hi=10 A window{A:1} == 1 -> have=3 -> VALID, shrink:
"DOBECODEBA" len 10 -> not better
drop D,O,B,E,C... each time re-check quota
when C is dropped: window{C:0} < 1 -> have=2, lo=6
hi=11 N have=2
hi=12 C window{C:1} -> have=3 -> VALID, shrink:
"ODEBANC" len 7 -> not better
... shrink to "BANC" len 4 -> best = (4, 9)
drop B: have=2, stop
answer = "BANC" ✓
| Complexity: O( | s | + | t | ) time — each index enters and leaves the window once — and |
| O( | alphabet | ) space. |
★ Insight ─────────────────────────────────────
haveis the whole design. Without it you would compare two dictionaries on every step, turning an O(n) scan into O(n · alphabet). Collapsing a multi-part validity condition into one counter is the move that makes hard window problems tractable — look for it whenever “valid” means “k separate conditions all hold”.- The
==in both updates is deliberate, not sloppy. Incrementing onwindow[ch] == required[ch](not>=) means a character that overshoots its quota does not inflatehave. Symmetrically, decrementing onwindow[left] < required[left](not<=) means dropping a surplus copy does not falsely invalidate the window. Swap either comparison and the answer is silently wrong on inputs with duplicates. - This is the shortest-flavour template (§5): grow unconditionally, and shrink while still valid, recording as you go. Compare with §4’s longest flavour, which shrinks while invalid and records after.
─────────────────────────────────────────────────
11. Problem 340 — Longest Substring with At Most K Distinct Characters
Return the length of the longest substring containing at most
kdistinct characters.s = "eceba",k = 2→3("ece").
The canonical “at most” window, and worth doing carefully because §12 turns it into a counting
tool. Grow right; while the window holds more than k distinct characters, shrink from the left.
The count dict’s size is the distinct count, so validity is O(1) to test.
def length_of_longest_substring_k_distinct(s, k):
if k == 0:
return 0
count = {}
lo = 0
best = 0
for hi, ch in enumerate(s):
count[ch] = count.get(ch, 0) + 1
while len(count) > k: # invalid -> shrink
left = s[lo]
count[left] -= 1
if count[left] == 0:
del count[left] # MUST delete, or len() lies
lo += 1
best = max(best, hi - lo + 1)
return best
s = "eceba", k = 2
hi=0 'e' count{e:1} len=1 <= 2 window "e" best=1
hi=1 'c' count{e:1,c:1} len=2 <= 2 window "ec" best=2
hi=2 'e' count{e:2,c:1} len=2 <= 2 window "ece" best=3
hi=3 'b' count{e:2,c:1,b:1} len=3 > 2 -> shrink:
drop 'e' -> count{e:1,c:1,b:1} still 3, lo=1
drop 'c' -> count{e:1,b:1} now 2, lo=2
window "eb" best=3
hi=4 'a' count{e:1,b:1,a:1} len=3 > 2 -> shrink:
drop 'e' -> count{b:1,a:1} now 2, lo=3
window "ba" best=3
answer = 3 ✓
Complexity: O(n) time, O(k) space.
★ Insight ─────────────────────────────────────
del count[left]when the count hits zero is load-bearing.len(count)is your validity test, and a key sitting at zero still counts towardlen. Leave it in and the window over-shrinks — a silent wrong answer, not a crash. This is the single most common bug in distinct-character windows.- The same skeleton with
len(count) > kswapped for a different predicate solves LC 3 (k = 1per character, i.e. all distinct), LC 159 (k = 2), and LC 904 (fruit baskets,k = 2in disguise). Recognising the disguise is most of the work.─────────────────────────────────────────────────
12. Counting Exactly K: the atMost Reduction
Here is a question the window template cannot answer directly: “how many subarrays have exactly K distinct values?” (LC 992). Windows are built to find a longest or shortest region, not to count regions, and “exactly K” is not a monotone property — growing a window can move you from exactly-K to exactly-(K+1) and there is no clean shrink rule.
The fix is a reduction that is worth memorising because it recurs constantly:
exactly(K) = atMost(K) - atMost(K - 1)
Every subarray with at most K distinct values either has at most K−1, or has exactly K.
Subtract and only the exactly-K ones survive. And atMost is monotone — it is the §11 window
verbatim, with one extra line.
The counting line is the second thing to internalise:
def subarrays_with_k_distinct(nums, k):
def at_most(limit):
if limit < 0:
return 0
count = {}
lo = 0
total = 0
for hi, x in enumerate(nums):
count[x] = count.get(x, 0) + 1
while len(count) > limit:
count[nums[lo]] -= 1
if count[nums[lo]] == 0:
del count[nums[lo]]
lo += 1
total += hi - lo + 1 # every window ending at hi is valid
return total
return at_most(k) - at_most(k - 1)
Why total += hi - lo + 1? Once the window [lo, hi] is valid, every suffix of it that
ends at hi is also valid — a shorter window cannot have more distinct values. Those suffixes are
[lo, hi], [lo+1, hi], ..., [hi, hi], and there are exactly hi - lo + 1 of them. So one addition
per step counts every valid subarray, with no double counting: each subarray is counted once, at
its own right endpoint.
nums = [1,2,1,2,3], k = 2
at_most(2):
hi=0 [1] lo=0 += 1 total=1
hi=1 [1,2] lo=0 += 2 total=3
hi=2 [1,2,1] lo=0 += 3 total=6
hi=3 [1,2,1,2] lo=0 += 4 total=10
hi=4 adds 3 -> 3 distinct, shrink lo to 2 ([1,2,3] still 3) then 3 -> [2,3] ok, lo=3
+= 4-3+1 = 2 total=12
at_most(1):
hi=0 [1] lo=0 += 1 total=1
hi=1 shrink to [2] lo=1 += 1 total=2
hi=2 shrink to [1] lo=2 += 1 total=3
hi=3 shrink to [2] lo=3 += 1 total=4
hi=4 shrink to [3] lo=4 += 1 total=5
exactly(2) = 12 - 5 = 7 ✓
Complexity: O(n) time (two linear passes), O(k) space.
★ Insight ─────────────────────────────────────
- Reduce non-monotone to monotone. The window needs a predicate where “valid” survives shrinking. “Exactly K” does not; “at most K” does. Whenever a constraint reads exactly, try expressing it as a difference of two at most problems before concluding windows do not apply. Same trick handles “exactly K odd numbers” (LC 1248) and “exactly K ones”.
total += hi - lo + 1is the counting idiom for the whole family, not a trick specific to this problem. Any time a window is valid and validity is inherited by suffixes, that one line counts every qualifying subarray in O(1) per step.─────────────────────────────────────────────────
13. Problem 862 — Shortest Subarray with Sum at Least K (negatives allowed)
Return the length of the shortest non-empty subarray with sum ≥
k, wherenumsmay contain negative numbers.nums = [2,-1,2],k = 3→3.
This problem exists to teach you where the plain window breaks. Section 5’s shortest-subarray template assumes that extending a window increases the sum, so that once you are at or above the target you can shrink. With negatives that is false: a longer window can have a smaller sum, so “shrink while valid” throws away windows that would have become valid later.
The repair is to stop working on the array and work on its prefix sums:
P[0] = 0, P[i] = nums[0] + ... + nums[i-1]
sum(nums[i..j-1]) = P[j] - P[i]
The question becomes: for each j, find the largest i < j with P[i] <= P[j] - k. Now run a
monotonic deque over the prefix array holding indices with increasing P values:
- Pop from the front while
P[j] - P[front] >= k. That front index gives a valid subarray, and it is the longest one still available — so record it and discard it, because any laterjwould only produce a longer answer with the same start. - Pop from the back while
P[j] <= P[back]. A later index with a smaller-or-equal prefix is strictly better as a future start: it is closer tojand subtracts less. The back index can never win again.
from collections import deque
def shortest_subarray(nums, k):
n = len(nums)
prefix = [0] * (n + 1)
for i, x in enumerate(nums):
prefix[i + 1] = prefix[i] + x
best = n + 1
dq = deque() # indices into prefix, increasing prefix values
for j in range(n + 1):
while dq and prefix[j] - prefix[dq[0]] >= k:
best = min(best, j - dq.popleft())
while dq and prefix[j] <= prefix[dq[-1]]:
dq.pop()
dq.append(j)
return best if best <= n else -1
nums = [2,-1,2], k = 3 -> prefix = [0, 2, 1, 3]
j=0 P=0 front: dq empty
back: dq empty dq=[0] (P: 0)
j=1 P=2 front: 2 - P[0]=0 -> 2 >= 3? no
back: 2 <= P[0]=0? no dq=[0,1] (P: 0,2)
j=2 P=1 front: 1 - 0 = 1 >= 3? no
back: 1 <= P[1]=2? yes -> pop 1
1 <= P[0]=0? no dq=[0,2] (P: 0,1)
j=3 P=3 front: 3 - P[0]=0 -> 3 >= 3? YES -> best = 3-0 = 3, popleft
3 - P[2]=1 -> 2 >= 3? no
back: 3 <= P[2]=1? no dq=[2,3]
answer = 3 ✓ (the whole array; no shorter subarray reaches 3)
Complexity: O(n) time — each index is pushed once and popped once — and O(n) space.
★ Insight ─────────────────────────────────────
- Negatives do not kill the window; they kill the array as the axis. Move to prefix sums and the monotonicity you need comes back, because now you are searching for a value in a structure you control rather than relying on the input’s sign.
- Two pops, two different reasons. The front pop harvests answers (this start works, and it is the longest still available). The back pop prunes dominated candidates (a smaller prefix arriving later beats a larger one arriving earlier on both axes). Every monotonic-deque problem has this same two-rule shape — see Stack & Queue §5 for the general form.
- Contrast with LC 209 (all-positive version): there the plain two-pointer window is enough, because with no negatives the prefix array is already increasing and the deque would never pop from the back.
─────────────────────────────────────────────────
14. Master Comparison Table
| Problem | Shape | Window state | Shrink condition | Record |
|---|---|---|---|---|
| 643 Max avg | Fixed k | running sum | (lockstep) | each step |
| 3 No repeats | Variable longest | last-seen index | duplicate in window | after shrink |
| 209 Min sum ≥ target | Variable shortest | running sum | while sum ≥ target |
inside shrink |
| 340 ≤ k distinct | Variable longest | char count map | len(map) > k |
after shrink |
| 438 / 567 Anagram | Fixed |p| | char count map | (fixed size) | on frequency match |
| 239 Window max | Fixed k | monotonic deque | index expired | front of deque |
| 424 Char replace | Variable longest | char count + max_freq | len - max_freq > k |
after shrink |
| 904 Fruit baskets | Variable longest | ≤ 2 distinct | len(map) > 2 |
after shrink |
| 1004 Max ones (flip k) | Variable longest | zero count | zeros > k |
after shrink |
What stays the same
1. right marches forward, one step per iteration
2. update window state with the entering element
3. restore validity by moving left (never backward)
4. record the answer at the right moment
What changes
Only three things: the state you track, the invalid test, and when you record. Everything else is boilerplate.
15. How to Identify This Pattern
Trigger: contiguous subarray/substring + optimize or count
See "contiguous subarray" or "substring" + one of:
├── "of size k" / "every k consecutive" → FIXED window (§3)
├── "longest ... such that <constraint>" → VARIABLE longest (§4, §7)
├── "shortest / minimum ... at least X" → VARIABLE shortest (§5)
├── "maximum in each window" → FIXED + monotonic deque (§8)
└── "matches a frequency / anagram" → FREQUENCY window (§6)
Sliding window vs. the alternatives
| If… | Use |
|---|---|
| Window updates in O(1) (add one / drop one), values non-negative | Sliding window |
| Array has negative numbers and you need sum constraints | Prefix sum + hash/deque (guide) |
| Two indices move but not as a resizing window (e.g. sorted pair-sum) | Two Pointers (guide) |
| Need max/min of window but not contiguous | Heap (guide) |
★ Insight ─────────────────────────────────────
- Sliding window is a special case of two pointers where both pointers only move forward and the region between them is the object of interest. Two pointers is broader (pointers may move toward each other, or over two arrays). If the window resizes based on a running constraint, it’s sliding window; if the pointers converge on a sorted structure, it’s two pointers.
─────────────────────────────────────────────────
Common Pitfalls
| Pitfall | Fix |
|---|---|
| Recording answer in wrong place | Longest → after shrink loop; shortest → inside shrink loop |
left moving backward |
It must be monotonic; guard stale duplicates with seen[ch] >= left |
| Using sliding window with negatives | Doesn’t work for sum constraints — switch to prefix sum |
| Off-by-one on window size | Size = right - left + 1; fixed window drops nums[right - k] |
| Rescanning the window for max | Use a monotonic deque (§8), not max(window) each step |
16. Practice Order
Start here
│
▼
643 (Easy) ──── Fixed window: the add-one/drop-one mechanic
│
▼
3 (Medium) ──── Variable longest: shrink on duplicate
│
▼
209 (Medium) ──── Variable shortest: shrink while valid
│
▼
438 (Medium) ──── Frequency window: Counter equality
│
▼
424 (Medium) ──── Longest with ≤ k violations (the max_freq trick)
│
▼
340 (Medium) ──── At-most-k distinct (remember to delete zero counts)
│
▼
239 (Hard) ──── Monotonic deque for window max
│
▼
76 (Hard) ──── Quota counter: collapse a multiset test into one int
│
▼
992 (Hard) ──── The atMost(k) - atMost(k-1) reduction
│
▼
862 (Hard) ──── Where the plain window dies: negatives + prefix deque
17. Pattern Recognition Cheat Sheet
Signal → shape
| You see in the problem statement | Shape | Key state |
|---|---|---|
| “every subarray of size k”, “average of k” | Fixed | running aggregate, add one / drop one |
| “longest substring such that …” | Variable, longest | grow always; shrink while invalid; record after the shrink |
| “minimum length subarray such that …” | Variable, shortest | grow always; shrink while valid; record before the shrink |
| “at most k distinct / k replacements / k zeros” | Variable + counter | len(count) or a violation tally |
| “anagram”, “permutation of t”, “contains all of t” | Fixed or variable + freq map | a have/need quota counter, not dict equality |
| “exactly k …” and it asks to count | Two windows | atMost(k) − atMost(k−1) (§12) |
| “maximum / minimum of each window” | Fixed + monotonic deque | indices in monotone value order (§8) |
| “sum ≥ target” with negative numbers | Not a plain window | prefix sums + deque (§13) |
| “subsequence” (not contiguous) | Not this pattern | → DP |
The two templates, side by side
The difference is one word in the while, and it flips where you record the answer:
# LONGEST: shrink while INVALID, record after
for hi in range(n):
add(hi)
while not valid():
remove(lo); lo += 1
best = max(best, hi - lo + 1) # window is valid here
# SHORTEST: shrink while VALID, record before
for hi in range(n):
add(hi)
while valid():
best = min(best, hi - lo + 1) # record, THEN break it
remove(lo); lo += 1
Bug checklist
- Did you delete zero-count keys? If
len(count)is your validity test, a lingering zero entry silently breaks it (§11). - Are you using
==or>=in the quota update? In LC 76,>=overcountshaveon duplicate characters (§10). - Does
leftever move backwards? It must not. If your logic needs it to, validity is not monotone and you need §12 or §13. - Fixed-size windows: are you recording only once the window is full? Emitting before
hi >= k-1produces answers for undersized windows. - Are there negatives? Check this explicitly before trusting a shrink-while-valid loop.
The Sliding Window Toolbox at a Glance
Contiguous region + monotone validity
│
┌────────────────────────┼────────────────────────┐
│ │ │
FIXED SIZE k VARIABLE NOT A PLAIN WINDOW
add hi, drop hi-k grow hi always the axis is wrong
│ │ │
│ ┌─────────┴─────────┐ │
│ LONGEST SHORTEST │
│ shrink while shrink while │
│ INVALID VALID │
│ │ │ │
643 avg k 3 no repeats 209 min sum 992 exactly-k -> atMost diff
438 anagram 424 k replace 76 min window 862 negatives -> prefix+deque
567 permutation 1004 k zeros 239 window max -> deque
340 k distinct
The one question that picks the branch: is the window size given (fixed), discovered (variable), or is the thing you need not a validity question at all (deque / reduction)?
When This Fails
The window requires monotone validity: shrinking a valid window must leave it valid. When that fails, the plain template silently returns a wrong answer rather than crashing.
- “Sum at least k” with negative numbers. Growing the window can decrease the sum, so shrink-while-valid discards windows that would have qualified later. Fix: prefix sums plus a monotonic deque (§13).
- “Exactly k distinct”. Not monotone in either direction. Fix:
atMost(k) − atMost(k−1)(§12). - The answer is a subsequence, not a substring. Windows only produce contiguous regions. That is DP territory.
- The window needs the maximum, not validity. “Max of each window” is not a constraint problem at all — it is a monotonic deque (Stack & Queue §5).
The tell that you are fighting the pattern: you want to move left backwards. That is never
legal in a sliding window, and wanting to is the signal to switch tools.
Self-Test
Answer these from memory, out loud or on paper, before looking. Recognition is not recall: rereading an explanation feels like knowing, and it is not. A question you cannot answer cold names the exact section to revisit — you do not need to reread the guide.
1. What property must hold for a sliding window to be correct, and what is the tell that it does not?
Answer
Monotone validity: removing an element from a valid window leaves it valid. The tell that it fails is wanting to move left backwards. left must only ever advance — that is what makes the whole scan O(n).
2. Why is the window O(n) despite the inner while loop?
Answer
Both pointers only advance, and each is bounded by n. So across the entire run they take at most 2n steps combined, regardless of how much the window resizes in between. This is amortized analysis, not a claim that each step is O(1).
3. Longest versus shortest: what is the one-word difference in the template?
Answer
Longest shrinks while invalid and records after the shrink loop (the window is valid at that point). Shortest shrinks while valid and records inside the loop, before breaking the window. Same skeleton, inverted condition, and the recording point moves with it.
4. You need to count subarrays with exactly K distinct values. Why can’t the window do it directly, and what is the fix?
Answer
“Exactly K” is not monotone — shrinking can drop you below K and growing can push you above, with no clean shrink rule. Fix: exactly(K) = atMost(K) − atMost(K−1). Every at-most-K subarray either has at most K−1 or exactly K, so the difference isolates the ones you want, and atMost is monotone.
5. In atMost, why is the count total += hi - lo + 1?
Answer
Once [lo, hi] is valid, every suffix ending at hi is also valid (a shorter window cannot have more distinct values). There are hi - lo + 1 such suffixes. Counting at each right endpoint means every subarray is counted exactly once.
6. What goes wrong if you leave zero-count keys in the frequency map?
Answer
len(count) is the validity test, and a key sitting at zero still counts toward len. The window over-shrinks — a silent wrong answer, not a crash. Always del when a count reaches zero.
7. In minimum window substring, why increment have on window[ch] == required[ch] rather than >=?
Answer
have counts distinct characters that have met quota. With >=, a character exceeding its quota would increment have again and falsely mark the window valid. Symmetrically the decrement uses < so that dropping a surplus copy does not falsely invalidate. Both comparisons break only on inputs with duplicates.
8. Negatives are present and you need the shortest subarray with sum ≥ k. What changes?
Answer
Stop using the array as the axis. Build prefix sums and run a monotonic deque over them: pop the front while P[j] − P[front] >= k (harvest an answer), pop the back while P[j] <= P[back] (prune a dominated candidate). Monotonicity returns because you now control the structure being searched.
See Also
- Two Pointers — the sibling technique; §11 draws the line between them.
- Prefix Sum — what to reach for when the array has negatives and the window loses its monotonicity.
- Stack & Queue §5 — the monotonic deque behind §8, with the amortized-O(1) argument in full.
- Heap / Priority Queue — for a window median or k-th value, where a deque cannot help.
- Binary Search — the fallback when the window is not monotone: search the answer and verify each candidate in a linear pass.
- Pattern Decision Map — the router: which technique does a cold problem call for?
- Pattern Mastery Program — the spaced-repetition schedule, mastery checklist, and drill formats that turn reading into recall.
Pattern mastered — one pointer marches, the other follows only to keep the window honest.