Minimum Window Substring — have/need Frequency Counter [LC 76, Google, Amazon, Facebook]
Advertisement
Problem Statement
LeetCode 76 — Minimum Window Substring · Difficulty: Hard
Given two strings
sandtof lengthsmandn, return the minimum window substring ofssuch that every character int(including duplicates) is included in the window. If there is no such substring, return the empty string"".
Constraints:
m == s.length,n == t.length1 <= m, n <= 10^5sandtconsist of uppercase and lowercase English letters
Example 1:
Input: s = "ADOBECODEBANC", t = "ABC"
Output: "BANC"
Explanation: "BANC" is the smallest window containing A, B, and C.Example 2:
Input: s = "a", t = "a"
Output: "a"Example 3:
Input: s = "a", t = "aa"
Output: ""
Explanation: "a" contains only one 'a', but "aa" requires two.Why This Problem Matters
LC 76 is one of the most important hard problems in FAANG interviews. Google, Amazon, Facebook, and Microsoft all ask it because it requires combining several ideas simultaneously: frequency counting, a two-pointer shrinkable window, and the have/need counter that tracks when the window is valid in O(1) per step.
The key insight that elevates this from O(n²) to O(n+m) is the have counter: instead of recomputing whether all characters of t are covered on every window change, you maintain a single integer that increments only when a character's count in the window exactly meets the requirement, and decrements only when it drops below.
This is the template for all "minimum window covering a required set" problems. Once you master LC 76, problems like LC 567 (Permutation in String), LC 438 (Find All Anagrams), and LC 30 (Substring with Concatenation of All Words) become straightforward.
The Core Insight
Step 1 — Build need. Count the frequency of each character in t. need is a frequency map: need[c] = how many times c must appear in the window.
Step 2 — have and needed. needed = number of distinct character types in t (i.e., len(need)). have = number of character types whose window count currently meets or exceeds the required count.
Step 3 — Expand right. For each new character s[right]:
- Increment
window[s[right]]. - If
window[s[right]] == need[s[right]], incrementhave(this type is now satisfied).
Step 4 — Shrink left (when valid). While have == needed (all types satisfied):
- Update the minimum window if the current one is smaller.
- Decrement
window[s[left]]. - If
window[s[left]] < need[s[left]], decrementhave(this type is no longer satisfied). - Advance
left.
Key invariant: have == needed means the current window covers all of t.
Visual Dry Run
Input: s = "ADOBECODEBANC", t = "ABC"
need = {A:1, B:1, C:1}, needed = 3
right | char | have | Action |
|---|---|---|---|
| 0 | A | 1 | window[A]=1==need[A]=1 → have++ |
| 1 | D | 1 | D not in need |
| 2 | O | 1 | O not in need |
| 3 | B | 2 | window[B]=1==need[B]=1 → have++ |
| 4 | E | 2 | E not in need |
| 5 | C | 3 | window[C]=1==need[C]=1 → have++; have==needed |
| — | Shrink | Record window "ADOBEC" (len=6), remove A → window[A]=0 < 1 → have-- | |
| 6 | O | 2 | Not in need |
| ... | ... | ... | ... |
| 9 | B | — | window[B]=2 ≥ 1, have stays |
| 10 | A | 3 | window[A]=1==need[A]=1 → have==needed |
| — | Shrink | Record "DOBEBANC"? Keep shrinking... eventually "BANC" (len=4) |
Final answer: "BANC"
Common Mistakes
-
Incrementing
havewhenwindow[c] >= need[c]instead of exactly==. Thehavecounter should increment only when the count first reaches the required level, not every time it increases beyond it. Using>=causeshaveto exceedneededand the window validity check breaks. -
Decrementing
havewhenwindow[c] < need[c]after removing from left. Only decrementhavewhen the count drops below the required level (fromneed[c]toneed[c]-1). Ifwindow[c]was already aboveneed[c], removing one doesn't break the constraint. -
Not checking if
s[left]is inneedbefore decrementinghave. Ifs[left]is not inneed, removing it from the window cannot invalidate any requirement. Always checkif c in needbefore modifyinghave. -
Using
need.sizeinstead ofneed.size()in JavaScript.Map.sizeis a property, not a method — useneed.size(notneed.size()). Calling it as a method returnsundefined, breaking the loop termination. -
Returning the wrong slice. The answer is
s[start : start + min_len], nots[start : start + min_len - 1]. Track the start index and the length; compute the slice at the end with both values. -
Not handling the case where
tcontains a character not ins. The algorithm handles this correctly —havenever reachesneededbecause the required character is never seen in the window. The function returns""as required.
Solutions
Python
from collections import Counter, defaultdict
def minWindow(s: str, t: str) -> str:
if not t or not s:
return ""
need = Counter(t) # required frequency of each character in t
needed = len(need) # number of distinct character types required
window = defaultdict(int) # frequency of characters in the current window
have = 0 # number of character types currently satisfied
left = 0 # left boundary of window
min_len = float('inf') # length of smallest valid window found
start = 0 # start index of smallest valid window
for right, c in enumerate(s):
window[c] += 1 # expand window: include s[right]
# Check if this character type is now satisfied
if c in need and window[c] == need[c]:
have += 1 # exactly met — increment have
# Shrink from left while window is valid (all types satisfied)
while have == needed:
# Update minimum window if current is smaller
if right - left + 1 < min_len:
min_len = right - left + 1
start = left
# Remove leftmost character from window
outgoing = s[left]
window[outgoing] -= 1
# If this drops below the required count, window is no longer valid
if outgoing in need and window[outgoing] < need[outgoing]:
have -= 1
left += 1 # advance left pointer
return "" if min_len == float('inf') else s[start : start + min_len]JavaScript
function minWindow(s, t) {
if (!s || !t) return "";
const need = new Map();
for (const c of t) {
need.set(c, (need.get(c) || 0) + 1); // build required frequency map
}
const needed = need.size; // distinct character types required
const window = new Map();
let have = 0; // character types currently satisfied
let left = 0;
let minLen = Infinity;
let start = 0;
for (let right = 0; right < s.length; right++) {
const c = s[right];
window.set(c, (window.get(c) || 0) + 1); // expand window
// Check if this character type is now exactly satisfied
if (need.has(c) && window.get(c) === need.get(c)) {
have++;
}
// Shrink from left while window is valid
while (have === needed) {
// Record this window if it is the smallest so far
if (right - left + 1 < minLen) {
minLen = right - left + 1;
start = left;
}
// Remove leftmost character
const outgoing = s[left];
window.set(outgoing, window.get(outgoing) - 1);
// If this drops below the required count, window becomes invalid
if (need.has(outgoing) && window.get(outgoing) < need.get(outgoing)) {
have--;
}
left++;
}
}
return minLen === Infinity ? "" : s.slice(start, start + minLen);
}Complexity Analysis
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute force (all substrings) | O(m²·n) | O(n) | Check each substring against t |
| Sliding window with have/need | O(m + n) | O(m + n) | Each pointer moves forward at most m times |
Each character in s is added to the window once (right pointer) and removed at most once (left pointer). The total work is O(m) for the main loop plus O(n) to build the need map. Space is O(m + n): O(n) for need and O(m) for window in the worst case.
Follow-up Questions
-
LC 567 — Permutation in String: Fixed-size window version — check if any window of length
|t|is an anagram oft. Samehave/needlogic, butleftadvances automatically whenright - left + 1 > |t|. -
LC 438 — Find All Anagrams in a String: Return all starting indices of windows that are anagrams. Same fixed-size window, collect start positions instead of minimum.
-
LC 30 — Substring with Concatenation of All Words: All words are the same length — slide a window in steps of word length, using word-frequency maps instead of character-frequency maps.
-
What if
tis very large (n >> m)? A valid window must contain all characters oft, so it has length at leastn. Ifn > m, no valid window exists — return""immediately.
This Pattern Solves
- LC 76 — Minimum Window Substring (this problem)
- LC 567 — Permutation in String (fixed-size window, anagram check)
- LC 438 — Find All Anagrams in a String (fixed-size window, collect starts)
- LC 3 — Longest Substring Without Repeating Characters (maximize window, one-type constraint)
- LC 159 — Longest Substring with At Most Two Distinct Characters (k-type constraint)
Key Takeaways
- LC 76 is asked at Google, Amazon, Facebook, and Microsoft — it is the hardest canonical sliding-window problem and appears at both phone screen and onsite rounds.
- Build
needas a frequency map oft; defineneeded = len(need)(distinct types) andhave(types whose window count meets the requirement). - Only increment
havewhenwindow[c] == need[c]exactly (first time this type is satisfied); only decrement whenwindow[c]drops belowneed[c]. - Shrink from the left whenever
have == needed— record the window if it is the smallest, then remove the leftmost character and advanceleft. - Both
leftandrightmove forward at mostmtimes; the total work is O(m + n). - Track
start(not justmin_len) when updating the minimum window; returns[start : start + min_len]at the end. - This
have/needcounter eliminates O(n) revalidation on every window change — the central optimization that makes the algorithm O(m) instead of O(m·n).
Advertisement