Minimum Window Substring
Counter-Based Sliding Window
The ‘hard’ sliding window — but if you understood #3, this is the same pattern with a counter.
Problem
Given two strings s and t, return the minimum window substring of s such that every character in t appears in the window with the same multiplicity.
If no such window exists, return the empty string "".
Intuition
This problem is a sliding window, but not the "find a substring with no duplicates" version from #3. Here the window is valid only when it contains enough of every required character.
So instead of a boolean invariant, we maintain a deficit counter:
need[c]tells us how many more of charactercthe window still needs.- When we add a character on the right, we reduce its deficit.
- When all deficits are satisfied, we try to shrink from the left as much as possible while staying valid.
That gives us the minimum window for every right boundary, and the best among those is the answer.
Approach
Counter-Based Sliding Window
O(n + m)O(1)Build the requirement counts from t, then sweep s with two pointers.
- Expand
rightuntil the window coverst. - Once valid, shrink
leftwhile the window still coverst. - Record the smallest valid window seen so far.
The window is valid when the number of still-missing characters reaches zero. Repeated characters are handled automatically because the counter tracks multiplicity, not just presence.
impl Solution {
pub fn min_window(s: String, t: String) -> String {
if t.len() > s.len() {
return String::new();
}
let sb = s.as_bytes();
let tb = t.as_bytes();
let mut need = [0i32; 128];
for &b in tb {
need[b as usize] += 1;
}
let mut missing = tb.len() as i32;
let mut left = 0usize;
let mut best_len = usize::MAX;
let mut best_start = 0usize;
for right in 0..sb.len() {
let idx = sb[right] as usize;
need[idx] -= 1;
if need[idx] >= 0 {
missing -= 1;
}
while missing == 0 {
let window_len = right - left + 1;
if window_len < best_len {
best_len = window_len;
best_start = left;
}
let left_idx = sb[left] as usize;
need[left_idx] += 1;
if need[left_idx] > 0 {
missing += 1;
}
left += 1;
}
}
if best_len == usize::MAX {
String::new()
} else {
s[best_start..best_start + best_len].to_string()
}
}
}function minWindow(s: string, t: string): string {
if (t.length > s.length) return "";
const need = new Int32Array(128);
for (let i = 0; i < t.length; i++) {
need[t.charCodeAt(i)]++;
}
let missing = t.length;
let left = 0;
let bestLen = Number.POSITIVE_INFINITY;
let bestStart = 0;
for (let right = 0; right < s.length; right++) {
const r = s.charCodeAt(right);
need[r]--;
if (need[r] >= 0) missing--;
while (missing === 0) {
const windowLen = right - left + 1;
if (windowLen < bestLen) {
bestLen = windowLen;
bestStart = left;
}
const l = s.charCodeAt(left);
need[l]++;
if (need[l] > 0) missing++;
left++;
}
}
return Number.isFinite(bestLen) ? s.slice(bestStart, bestStart + bestLen) : "";
}The important detail is the sign convention on need: positive means the
window is still missing that many characters, zero means satisfied, negative
means the window has surplus copies.
Trace
For s = "ADOBECODEBANC", t = "ABC":
| right | char | missing | action | best |
|---|---|---|---|---|
| 0 | A | 2 | gain A | - |
| 1 | D | 2 | surplus | - |
| 2 | O | 2 | surplus | - |
| 3 | B | 1 | gain B | - |
| 4 | E | 1 | surplus | - |
| 5 | C | 0 | window valid, shrink left to 1 | ADOBEC |
| 6-9 | ... | ... | keep shrinking and expanding | ADOBEC |
| 10 | A | 0 | window valid again | BANC |
The first valid window is not the answer; it is only the first chance to shrink.
Edge cases
- No possible window returns
"". - Repeated letters in
tmatter.t = "AABC"requires twoAs, not one. tlonger thanscan short-circuit immediately.- Surplus characters inside the window are allowed as long as the deficits stay covered.
If #3 is "maintain a set of characters that are inside the window," #76 is "maintain a multiset of required characters that the window still owes." Same loop, stricter invariant.
Takeaway
The difference between easy and hard sliding window problems is usually the invariant, not the loop shape.
Once you can phrase the window state as a counter of remaining requirements, minimum-window problems stop being mysterious and become a mechanical shrink-and-record scan.
Your first sliding window. When do you shrink vs expand?
Converging pointers from both ends. Simple but builds the muscle memory.
Why does moving the left pointer increase the sum? Understand the proof.
More in Two Pointers & Sliding Window
Converging pointers from both ends. Simple but builds the muscle memory.
Why does moving the left pointer increase the sum? Understand the proof.
Fix one element, two-pointer the rest. Duplicate handling is the real challenge.
Your first sliding window. When do you shrink vs expand?