Minimum Window Substring
Worked solution, explanation, and GitHub source for this problem.
76. Minimum Window Substring
LeetCode: Problem #76
Difficulty: Hard
Topics: String Sliding Window Hash Map Two Pointers
Problem Statement
Given two strings s and t, return the minimum window substring of s
such that every character in t (including duplicates) is included in the
window. If no such window exists, return an empty string "".
Constraints:
1 <= s.length, t.length <= 10⁵sandtconsist of uppercase and lowercase English letters- The answer is guaranteed to be unique
Example
Input: s = "ADOBECODEBANC", t = "ABC"
Output: "BANC"
Explanation: "BANC" is the smallest window containing 'A', 'B', and 'C'.
Input: s = "a", t = "a"
Output: "a"
Input: s = "a", t = "aa"
Output: ""
Explanation: 't' requires two 'a's; 's' has only one.
How to Think About This Problem
Step 1 — Understand what's being asked
Find the shortest contiguous substring of s that contains all characters
of t at least as many times as they appear in t. Duplicates in t matter.
Step 2 — Identify the constraint that matters
We need every character in t satisfied, with correct frequencies. As we
expand the window, we track how many of t's distinct character requirements
are fully met ("formed"). Once all are met, we try to shrink from the left.
Step 3 — Think about data structures
Two hash maps — need (required frequencies from t) and have (current
window frequencies) — let us check in O(1) whether a character's count just
crossed the threshold. A formed counter tracks how many distinct characters
are fully satisfied. When formed == len(need), we have a valid window.
Step 4 — Build the intuition
Expand the right pointer one step at a time. When a character's count in have
reaches its count in need, increment formed. Once formed == required,
the window is valid — record its length, then shrink from the left until the
window becomes invalid again. Repeat.
This "expand until valid, shrink until invalid" rhythm is the core sliding window pattern.
Approaches
Approach 1 — Brute Force
Intuition: Check every possible substring of s of length ≥ len(t).
Steps:
- Generate all substrings of
swith length>= len(t) - For each, check if it contains all characters of
t - Track the shortest valid one
Complexity:
- Time: O(n² · |t|) — O(n²) substrings, each checked in O(|t|)
- Space: O(|t|) — character frequency map
Code:
from collections import Counter
def minWindow_brute(s, t):
need = Counter(t)
result = ""
for i in range(len(s)):
for j in range(i + len(t), len(s) + 1):
window = Counter(s[i:j])
if all(window[c] >= need[c] for c in need):
if not result or j - i < len(result):
result = s[i:j]
return result
Approach 2 — Optimal (Sliding Window)
Intuition: Use a variable-width sliding window. Expand right to satisfy all requirements, then shrink left as much as possible while still valid.
Steps:
- Build
need = Counter(t),required = len(need) have = {},formed = 0,left = 0, track min window- Expand
right:- Add
s[right]tohave - If
have[s[right]] == need[s[right]], incrementformed
- Add
- While
formed == required:- Update min window if current is shorter
- Remove
s[left]fromhave; ifhave[s[left]] < need[s[left]], decrementformed - Advance
left
Illustration:
s = "ADOBECODEBANC", t = "ABC"
need = {A:1, B:1, C:1}, required = 3
Expand right until all 3 satisfied:
r=0 (A): have={A:1}, formed=1
r=1 (D): have={A:1,D:1}
r=2 (O): have={A:1,D:1,O:1}
r=3 (B): have={...,B:1}, formed=2
r=4 (E): ...
r=5 (C): have={...,C:1}, formed=3 → valid! window="ADOBEC" (len 6)
Shrink left:
l=0 (A): have[A]=0 < need[A]=1 → formed=2, stop shrinking. l=1
window "DOBECODEBA..." not checked yet; formed=2 now.
Expand right again:
r=6 (O): ...
r=7 (D): ...
r=8 (E): ...
r=9 (B): have[B]=2
r=10 (A): have[A]=1 → formed=3 again! window="DOBECODEBA" (len 10) — worse
Shrink left (l=1..):
l=1 (D): remove D, not in need → shrink. l=2
l=2 (O): remove O, not in need → shrink. l=3
l=3 (B): have[B]=1, was 2 → still >= need[B]=1 → shrink. l=4
l=4 (E): remove E → shrink. l=5
l=5 (C): have[C]=0 < need[C]=1 → formed=2, stop. l=6
Best window so far: s[3:10]="OBECODEBA" wait... let me recount.
...eventually window "BANC" (indices 10..13) is found with len=4 ✓
Complexity:
- Time: O(|s| + |t|) — each character is added and removed from the window at most once
- Space: O(|s| + |t|) — hash maps
Code:
from collections import Counter
def minWindow(s, t):
if not t:
return ""
need = Counter(t)
have = {}
formed = required = 0
required = len(need)
left = 0
min_len = float('inf')
result = ""
for right, char in enumerate(s):
have[char] = have.get(char, 0) + 1
if char in need and have[char] == need[char]:
formed += 1
while formed == required:
if right - left + 1 < min_len:
min_len = right - left + 1
result = s[left:right + 1]
left_char = s[left]
have[left_char] -= 1
if left_char in need and have[left_char] < need[left_char]:
formed -= 1
left += 1
return result
Solution Breakdown — Step by Step
from collections import Counter
def minWindow(s: str, t: str) -> str:
if not t:
return ""
need = Counter(t)
have = {}
formed = 0
required = len(need)
left = 0
min_len = float('inf')
result = ""
for right, char in enumerate(s):
have[char] = have.get(char, 0) + 1
if char in need and have[char] == need[char]:
formed += 1
while formed == required:
if right - left + 1 < min_len:
min_len = right - left + 1
result = s[left:right + 1]
left_char = s[left]
have[left_char] -= 1
if left_char in need and have[left_char] < need[left_char]:
formed -= 1
left += 1
return result
Line by line:
need = Counter(t)
- Frequency map of what we need —
{'A': 1, 'B': 1, 'C': 1}fort = "ABC"
required = len(need)
- Number of distinct characters we need to fully satisfy (not total characters)
have[char] = have.get(char, 0) + 1
- Track how many of each character the current window contains
if char in need and have[char] == need[char]: formed += 1
- Only increment when we've exactly hit the required count — not exceeded it
- This avoids double-counting:
have[char] == need[char]is true only on the crossing moment, not every time we add another copy
while formed == required:
- All characters are fully satisfied — try to shrink the window
- Inner loop runs the "minimize" logic while valid
if right - left + 1 < min_len:
- Check before shrinking — current window is valid, record it if it's the best
have[left_char] -= 1
- Remove the leftmost character from the window
if left_char in need and have[left_char] < need[left_char]: formed -= 1
- If removing this character drops a required count below its threshold,
the window is no longer valid — decrement
formedto exit the while loop
left += 1
- Advance the left boundary
Quick Summary
| Approach | Time | Space |
|---|---|---|
| Brute Force (all substrings) | O(n² · |t|) | O(|t|) |
| Optimal (sliding window) | O(|s| + |t|) | O(|s| + |t|) |
Common Mistakes
1. Incrementing formed on every character match instead of threshold crossing
# WRONG — formed gets incremented every time we see a needed character
if char in need:
formed += 1
# CORRECT — only when the count exactly meets the requirement
if char in need and have[char] == need[char]:
formed += 1
If t = "AA" and need = {A: 2}, adding the first A should not count as
formed. Only when have[A] == 2 do we count it.
2. Checking total length of t instead of distinct character count
# WRONG — len(t) counts duplicates; required should be distinct keys
required = len(t) # if t = "AAB", this is 3
# CORRECT
required = len(need) # if t = "AAB", this is 2 (A and B)
3. Shrinking the left without recording the window first
# WRONG — shrinks before checking if this is the minimum
left += 1
if right - left + 1 < min_len: # window already smaller than when valid
Always record the window size before advancing left.
Pattern Recognition
How to Recognize
- "Minimum/maximum substring/subarray" with a constraint on what it must contain
- The window must include all elements of a target set with correct frequencies
- Expand right to satisfy, shrink left to minimize
How to Identify
- Is there a "formed / required" count that tells you when a window is valid?
- Does expanding always get you closer to valid, shrinking always toward invalid?
How to Remember
Mental model: Fill a bag (expand right) until it has everything you need, then empty from the front (shrink left) until something falls short. Record every valid bag. Repeat.
Similar problems:
- Permutation in String (LeetCode #567) — fixed-size window with same frequency check
- Longest Substring Without Repeating Characters (LeetCode #3) — simpler sliding window
- Substring with Concatenation of All Words (LeetCode #30) — same pattern, word-level
Real World Use Cases
1. DNA subsequence search in bioinformatics
Finding the shortest segment of a genome that contains all marker nucleotides of a target sequence — used in primer design and variant detection pipelines where the target pattern must appear within a window.
2. Log file analysis
In observability tools, finding the shortest time window in a log stream that contains all required event types (e.g., request start, DB call, response) — used to detect the fastest possible end-to-end trace.
3. Text document retrieval
Search engines checking whether a document contains all query terms within a short passage (proximity search). The minimum window gives the most relevant snippet to display as the search result excerpt.
Key Takeaways
- Track
formed(satisfied requirements) separately fromhave(raw counts) - Only update
formedwhen a count exactly hits the threshold — not every time required = len(need)counts distinct characters, not total characters int- Expand right to satisfy, shrink left to minimize — the classic sliding window rhythm
- This pattern handles duplicate requirements in
tnaturally via the frequency maps
Where to Practice
| Platform | Problem | Difficulty |
|---|---|---|
| LeetCode #76 | Minimum Window Substring | Hard |
Part of the Blind 75 and NeetCode 150 interview prep lists.