Minimum Window Substring
Problem (restated)
Given strings s and t, return the minimum-length substring of s that covers every character of t (with at least the same multiplicities). If no such window exists, return the empty string. Any minimum window is acceptable if several share the same length.
Intuition
This is the min-window shape of sliding window, not the “longest valid” shape:
- Expand
rightuntil the window is valid (coverst). - While it stays valid, shrink
leftand track the best (shortest) window. - When shrinking breaks validity, expand again.
The invariant is “window contains all required counts from t.”
Approaches
Sliding window with need/have counts
Tested onlyIdea. Build need frequencies from t and a missing (or have/needTypes) counter. Move right, decrement need for that char when useful. When missing == 0, the window is valid: record s[left..right], then advance left and restore need until invalid.
Walkthrough. s = "ADOBECODEBANC", t = "ABC".
| step | window (idea) | notes |
|---|---|---|
| expand to first full cover | ADOBEC |
first valid, length 6 |
| shrink while valid | still need a full cover | track best |
| later expand/shrink | BANC |
better length 4 → answer |
Trade-offs. Still O(|s|) once each end moves at most once. Bookkeeping is the hard part: confuse “need” vs “window count” and you under-shrink or accept incomplete covers. Fixed-size alphabet arrays beat hash maps for ASCII.
export function minWindow(s: string, t: string): string {
if (!t) return "";
const need = new Map<string, number>();
for (const ch of t) need.set(ch, (need.get(ch) ?? 0) + 1);
let missing = need.size;
let left = 0;
let bestL = 0, bestLen = Infinity;
const window = new Map<string, number>();
for (let right = 0; right < s.length; right++) {
const ch = s[right]!;
window.set(ch, (window.get(ch) ?? 0) + 1);
if (need.has(ch) && window.get(ch) === need.get(ch)) missing--;
while (missing === 0) {
if (right - left + 1 < bestLen) {
bestLen = right - left + 1;
bestL = left;
}
const leftCh = s[left]!;
window.set(leftCh, (window.get(leftCh) ?? 0) - 1);
if (need.has(leftCh) && (window.get(leftCh) ?? 0) < need.get(leftCh)!) missing++;
left++;
}
}
return bestLen === Infinity ? "" : s.slice(bestL, bestL + bestLen);
}
export function minWindow(s: string, t: string): string {
if (!t) return "";
const need = new Map<string, number>();
for (const ch of t) need.set(ch, (need.get(ch) ?? 0) + 1);
let missing = need.size;
let left = 0;
let bestL = 0, bestLen = Infinity;
const window = new Map<string, number>();
for (let right = 0; right < s.length; right++) {
const ch = s[right]!;
window.set(ch, (window.get(ch) ?? 0) + 1);
if (need.has(ch) && window.get(ch) === need.get(ch)) missing--;
while (missing === 0) {
if (right - left + 1 < bestLen) {
bestLen = right - left + 1;
bestL = left;
}
const leftCh = s[left]!;
window.set(leftCh, (window.get(leftCh) ?? 0) - 1);
if (need.has(leftCh) && (window.get(leftCh) ?? 0) < need.get(leftCh)!) missing++;
left++;
}
}
return bestLen === Infinity ? "" : s.slice(bestL, bestL + bestLen);
}
Template connection
Opposite of the default max-window template: there you shrink while invalid and always update length when valid. Here you shrink while valid and only then update the minimum. See the sliding-window template header for both shapes.
Common bugs
- Tracking presence (set) instead of counts when
thas duplicates. - Updating the answer before the window is fully valid.
- Off-by-one when computing length
right - left + 1after shrinking.
Reflection
- Did you default to longest-window by habit? What cue says “minimum cover”?
- What breaks if
thas duplicates and you only track presence (set) instead of counts? - Empty
t/ no cover cases: what should you return?