Minimum Window Substring is the classic "hard" sliding-window problem. It teaches the two-phase window: grow until valid, shrink while valid — and a counter trick to check validity in O(1).
Problem. Given two strings s and t, return the shortest substring of s that contains
every character of t (including duplicates). If no such window exists, return "".
Example: s = "ADOBECODEBANC", t = "ABC" → answer "BANC" (the smallest piece of s containing
an A, a B and a C).
The slow way first
The brute force: try every possible substring of s, and for each one check whether it covers t. There are O(n²) substrings, and each check costs O(n), so this is O(n³) — hopeless for long strings.
The question to ask: can I slide a window across s once, instead of re-checking every substring from scratch? Yes — and the key is tracking, in O(1), how close the current window is to being valid.
The idea: grow to satisfy, shrink to minimize
Keep a window [L, R]. Build a need count of every character in t, and a single number missing = how many characters of t are still uncovered.
- Grow R: extend the window one character at a time. Each time we add a character that
tstill needs,missingdrops by one. - When
missing == 0the window is valid — it covers all oft. Record it if it is the smallest so far, then shrink L to make it smaller. - Shrink L: drop characters from the left as long as the window stays valid. The moment removing a character breaks validity, stop and go grow R again.
The whole window never moves backward: L and R each only travel left-to-right across s, so the total work is O(n).
Walk through it
Step through the animation. R races right until the window "ADOBEC" first covers A, B and C — that is our first valid window (length 6), so we record it. Then L shrinks: dropping the A breaks validity, so R grows again. We repeat this grow/shrink dance. Eventually the window tightens to "BANC" (length 4), beating 6, and that becomes the answer.
Pseudocode
need = count of each character in t
missing = len(t) # chars still uncovered
best = ""
L = 0
for R, ch in s:
if t still needs ch: missing -= 1
need[ch] -= 1
while missing == 0: # window [L..R] is valid
if best is "" or window is smaller: best = s[L..R]
need[s[L]] += 1 # give the left char back
if need[s[L]] > 0: missing += 1 # window just broke
L += 1
return bestThe Python solution
def min_window(s, t):
need = Counter(t)
missing = len(t)
best = ""
L = 0
for R, ch in enumerate(s):
if need[ch] > 0:
missing -= 1
need[ch] -= 1
while missing == 0: # window is valid
if best == "" or R - L + 1 < len(best):
best = s[L:R + 1]
need[s[L]] += 1
if need[s[L]] > 0:
missing += 1 # window broke
L += 1
return bestneedis aCounteroft; it can go negative for characters the window has in excess, which is exactly what makes the shrink check work.missingis the single source of truth for validity:missing == 0means every required character is covered.- We only decrement
missingwhenneed[ch] > 0— i.e. the character was still actually required, not a surplus. - The inner
whiledoes all the shrinking. We recordbestfirst, then pop the left character; if its count climbs back above 0, the window broke and we leave the loop.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (all substrings) | O(n³) (moderate) | O(n²) windows × O(n) check |
| Sliding window (this solution) | O(n) (moderate) | L and R each cross s once |
O(k) (moderate)Space is O(k) where k is the number of distinct characters in t (the size of the counter). The time win is enormous: O(n³) → O(n), because each pointer only moves forward.
When this pattern shows up
Reach for the grow/shrink sliding window whenever a problem asks for the shortest or longest contiguous segment satisfying a constraint: "smallest substring containing all of t," "longest substring without repeats," "shortest subarray with sum ≥ target." A running counter plus a single validity number turns the O(1) check into the engine of the whole scan.
The subtle bug is the validity counter. Only decrement missing when the character was genuinely
still needed (need[ch] > 0 before decrementing), and only increment it back when removing a left
character pushes its count strictly above 0. Letting surplus characters touch missing silently
breaks the answer.
Practice
For s = 'ADOBECODEBANC', t = 'ABC', the first valid window the grow phase finds is 'ADOBEC' (length 6). What is the final answer, and why is it shorter?
1. Why is the sliding window O(n) and not O(n²)?
2. What does the single number missing represent?
3. Why can the need counter go negative?
4. In the inner while loop, when do we stop shrinking L?