DSA sheet · Strings · Sliding Window pattern
Minimum Window Substring
LeetCode marks this one Hard, but the teacher's promise is that, taught step by step, it feels like an easy-medium. She builds it in three stages: a brute force (every start, a window map, and a "does it cover t?" check), a sliding window with two maps plus a count of characters still needed, and finally the same window with only one map, which saves space.
Why it matters: this is the classic "shortest valid window" problem. Longest-window problems shrink only when the window breaks. Here it's the opposite: we shrink while the window is still valid, to squeeze it as small as possible. On top of her method, this page also covers the common formed / required way of writing the counter, which you'll see in many editorials.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting (sliding window from scratch)
- Part A · Brute force: every start + a window map + a cover check
- Part B · Sliding window with two maps + a "still needed" count (her main method)
- Part C · The same window with a formed / required counter
- Part D · Space-optimised: one map with negative counts
- Part E · Revision page
Part 0 · Before starting
Substring = a contiguous piece
A substring is a piece of the string with no gaps: choose a start and an end and take everything between. (For arrays it's a subarray.) A string of length n has n·(n+1)/2 non-empty substrings, so trying them all is O(n²).
Brute force over all windows, and why it repeats work
Two loops: every start i, every end j. When i moves one step, the inner loop rebuilds and re-checks characters the previous round already processed. Sliding window keeps the current window and only adjusts its two edges.
The window [left..right], expanding and shrinking
- Expand:
rightmoves forward and the new character is added to our bookkeeping. - Shrink:
leftmoves forward and the character at left is removed.
The teacher's rule again: right is responsible for expanding, left for shrinking, and left undoes exactly what right did. Both only move forward. Length = right − left + 1.
Fixed-size vs variable-size
| Fixed-size | Variable-size | |
|---|---|---|
| when | length k is given (e.g. Find All Anagrams: windows of length |p|) | length is free; find the longest / shortest window obeying a rule |
| this problem | ✓ we want the shortest window that covers t |
Longest vs shortest vs count: when do we shrink?
| asks for | shrink when… | record the answer… |
|---|---|---|
| longest valid | the window is invalid (shrink to repair it) | after shrinking, when it's valid again |
| shortest valid (this problem) | the window is valid (shrink to make it smaller) | inside the shrinking loop, each time before removing |
| count of valid | invalid | count += right − left + 1 |
(For "count with exactly K", there's also the exactly(K) = atMost(K) − atMost(K−1) trick. Not needed here.)
Why sliding window works here (the monotonic rule)
"The window covers t" means every character of t is inside, with at least the same number of copies. Adding a character can never break coverage. Removing one can only keep it or break it. So:
- if [left..right] covers t, every bigger window around it also covers t (but it's longer, so it's useless to us);
- if [left..right] doesn't cover t, every smaller window inside it doesn't either.
That one-way behaviour is what lets both pointers move only forward. (Sum problems lose it when numbers can be negative, which is why those need a different pattern.)
Frequency maps (Counter)
A frequency map stores character → how many times. In Python, collections.Counter(t) builds it in one line: Counter("AABC") = {A:2, B:1, C:1}. For coverage we compare counts with "at least": window[c] ≥ need[c] for every c in t. Extra copies, and extra characters that aren't in t, are allowed.
def shortest_valid(s, add, remove, is_valid):
left = 0
best_len, best_left = float('inf'), 0
for right in range(len(s)):
add(s[right]) # expand
while is_valid(): # valid -> try to make it smaller
if right - left + 1 < best_len:
best_len, best_left = right - left + 1, left
remove(s[left]) # shrink
left += 1
return "" if best_len == float('inf') else s[best_left:best_left + best_len]Part A · Brute force: every start + a cover check
LeetCode 76
1The question in simple words
You get two strings, s (length m) and t (length n). Return the shortest substring of s that contains every character of t, duplicates included. Other characters may also be inside. If no such substring exists, return the empty string "".
"ADOBEC" (index 0..5) also contains A, B and C, but its length is 6. "BANC" has length 4, the shortest. "Duplicates included" means that if t = "AA", the window needs two A's. LeetCode's other examples: s = "a", t = "a" → "a", and s = "a", t = "aa" → "" (only one a exists).
2What the constraints tell us
- m, n up to 10⁵. O(n²) = 10¹⁰, far past ~10⁸ → brute force will TLE. We need about O(m + n).
- s and t hold upper- and lower-case letters. In general, the teacher counts the possible characters as up to 256 (lower case, upper case, symbols). That bounds the size of any map: it's a constant.
- t can be longer than s → then no window can cover it →
"".
3Intuition: grow from each start until it covers t
First build tMap, the count of each character in t. Then fix a start i, grow j, and keep a window map of s[i..j]. After each new character, ask: "does the window map have at least as many of each character as tMap?" The first j where the answer is yes gives the shortest valid window for this start. Going further only makes it longer, so we break and move i.
4Building the logic from the example
i = 0
tMap = {A:1, B:1, C:1}. "A" → has A, no B or C. "AD", "ADO", "ADOB" (A, B ✓, C missing), "ADOBE", then "ADOBEC" → A, B and C all there ✓. Length 6, our first answer.
→ No. Adding more characters keeps it valid but makes it longer, and we want the minimum. So for this start we break. (Compare with K-unique, where we wanted the longest and had to keep going.)
i = 1
Fresh window map. "D", "DO", "DOB", "DOBE", "DOBEC" (B, C ✓, no A), "DOBECO", "DOBECOD", "DOBECODE", "DOBECODEB", "DOBECODEBA" → now A is in too ✓. The window holds B twice while t needs it once.
→ Yes. The rule is "at least" as many as t needs. t asked for one A, one B, one C. The window gives two B's, one A, one C, so t's demand is met. It's valid, but length 10 isn't better than 6, so we don't update. Then break.
i = 2 and onwards
Same thing: from index 2 the first covering window is "OBECODEBA" (9). And so on. The shortest one appears from i = 9: "BANC" (4).
The cover check (her isValid / match function)
Loop over every character c of tMap. If the window has fewer c's than tMap needs (e.g. t needs B:3 but the window has B:2), return False. If every character passes, return True.
Updating the answer
Update when the new valid window is shorter than the stored one. The teacher points out a trap: if the answer starts as the empty string "", then "shorter than the answer" means shorter than length 0, which never happens. So the first valid window must be accepted when the answer is still empty. (In code below, we keep a best length that starts at infinity, which avoids the trap.)
5Approach steps
- Build
need = Counter(t). - For each start i: empty window map.
- For j from i: add s[j] to the window.
- If the window covers need: if j − i + 1 is shorter than the best, save it. Break.
- Return the best substring, or "".
6Code (Python)
from collections import Counter
class Solution:
def minWindow(self, s, t):
need = Counter(t) # tMap
best_len, best_left = float('inf'), 0
for i in range(len(s)):
window = {} # sMap for s[i..j]
for j in range(i, len(s)):
window[s[j]] = window.get(s[j], 0) + 1
if self.covers(window, need):
if j - i + 1 < best_len:
best_len, best_left = j - i + 1, i
break # longer ones can't be better
return "" if best_len == float('inf') else s[best_left:best_left + best_len]
def covers(self, window, need):
for c in need: # at most 256 different chars
if window.get(c, 0) < need[c]: # not enough copies of c
return False
return True7Code line by line
| line | what it means |
|---|---|
| need = Counter(t) | tMap: how many of each character t needs. One pass over t, O(n). |
| best_len = float('inf') | "No answer yet". Any real window is shorter than infinity, so the first valid one is always saved (the empty-answer trap is gone). |
| window = {} | A new map for every start. |
| window[s[j]] = … + 1 | Grow the window by s[j]. |
| if self.covers(window, need): | Has every needed character, with enough copies? |
| if j - i + 1 < best_len: … | Shorter than what we had → save its start and length. |
| break | For this start, any longer window is useless. |
| window.get(c, 0) < need[c] | The "at least" check. Extra copies are fine; too few is not. |
→ Cutting a substring copies it, which costs its length each time. Saving two numbers is O(1), and we cut once at the very end. Same answer, less work. Python-specific tidy-up, not a change to her logic.
8Dry run (hand table)
s = "ADOBECODEBANC", t = "ABC".
| i | first j where the window covers t | window | length | best so far |
|---|---|---|---|---|
| 0 | 5 | ADOBEC | 6 | ADOBEC (6) |
| 1 | 10 | DOBECODEBA | 10 | 6 |
| 2 | 10 | OBECODEBA | 9 | 6 |
| 3 | 10 | BECODEBA | 8 | 6 |
| 4 | 10 | ECODEBA | 7 | 6 |
| 5 | 10 | CODEBA | 6 (not shorter) | 6 |
| 6 | 12 | ODEBANC | 7 | 6 |
| 7 | 12 | DEBANC | 6 | 6 |
| 8 | 12 | EBANC | 5 | EBANC (5) |
| 9 | 12 | BANC | 4 | BANC (4) |
| 10–12 | never (no B after index 9) | — | — | 4 |
Final answer: "BANC" ✓
9Complexity & remember
- O(n) to build tMap, then two nested loops O(m²), and each step calls the cover check over up to 256 characters → O(m² · 256), which is O(m²) since 256 is a constant. For m = 10⁵ → TLE, as she shows.
- Space O(256) for the two maps.
Part B · Sliding window: two maps + a "still needed" count
1The question again, with the new goal
Same question. Goal: O(m + n), with each pointer walking s once, and no cover-check loop on every step.
2What the constraints tell us now
- m up to 10⁵ → linear time.
- Maps hold at most 256 keys → O(1) space in big-O terms.
3Intuition: where is the repeated work?
From i = 1 the brute force found "DOBECODEBA" valid. When i moved to 2, it removed D and rebuilt everything from scratch. But D wasn't even needed! Without checking anything, we already know "OBECODEBA" is valid too. Then O is removed, also not needed, so "BECODEBA" is valid. Re-checking those is pure repetition.
So: keep one window. Right grows it until it covers t. Then left shrinks it as long as it still covers t, recording the size each time. When it stops covering, right grows again. It's sliding window and not two pointers because we care about the whole substring between the pointers, not just the two characters at the ends.
4Building the logic: the count, and why frequency matters
Getting rid of the cover check: a counter
Calling a match function after every step is slow. Instead, keep a number: count = how many characters of t are still missing. It starts at len(t) (3 for "ABC"). When count == 0, nothing is missing, so the window is valid. No loop needed.
First version of the rule, and where it breaks
First idea: when right brings in a character that's in t, do count −= 1. Walk it: A → 2, D (not in t), O (not in t), B → 1, E, C → 0. Valid: "ADOBEC". Record 6, then shrink: A leaves, and A is in t, so count += 1 → 1. Now left is at D.
Right continues: O, D, E (not in t), then B. B is in t, so the naive rule says count −= 1 → 0, "valid". But the window "DOBECODEB" has no A! What went wrong? The window already had a B (index 3). This second B is an extra copy; t asked for only one. It doesn't fill anything that was missing.
window[c] += 1. If c is in t and window[c] ≤ need[c] (this copy is still a required one, not an extra), then count −= 1.Left removes c: if c is in t and
window[c] ≤ need[c] before removing (we're losing a required copy), then count += 1. Then window[c] −= 1.With this rule, the second B makes window[B] = 2 > need[B] = 1 → an extra copy → count stays 1. Correct. Then A arrives: window[A] = 1 ≤ 1 → count 0 → now valid ("DOBECODEBA").
Why do we shrink while the window is VALID?
This is the heart of the problem:
- When the window first becomes valid at some right, moving right further only makes longer windows. They're valid but useless for a minimum.
- The only way to find a shorter valid window ending at this right is to move left. Each step removes one character. If the window is still valid, it's a new, smaller candidate → record it, and keep going.
- The moment it becomes invalid (count > 0), we've lost a needed character. No smaller window ending here can be valid (it's missing that character too). So we stop shrinking and let right look for the missing character.
That's why the loop is while count == 0: (record, remove, move left), the opposite of the longest-window problems, where we shrink only while the window is broken.
→ At the top of the loop, the window [left..right] is valid (count == 0). That's the candidate. After we remove s[left], the window may be invalid, so we must record first. The loop check then decides whether the smaller window is still valid.
count start at len(t) and not at the number of different characters?→ In her version, count counts copies. t = "AAB" needs 3 copies (two A's and a B), so count starts at 3, and each required copy that arrives lowers it by 1. The "≤ need" check stops extra copies from counting. (Part C shows the other way, counting distinct characters.)
Walking the shrink on the example
At right = 10 the window "DOBECODEBA" is valid. Shrink: D leaves (not needed) → "OBECODEBA" still valid. O leaves → still valid. B(3) leaves: window[B] was 2 > need 1, so it was an extra copy → count stays 0 → "ECODEBA" still valid. E leaves → "CODEBA" valid, length 6. C leaves: window[C] was 1 ≤ 1 → a required copy → count = 1 → stop. Right moves on: N, then C → count 0 → shrink O, D, E → "BANC" (4) → B leaves → count 1. Right reaches the end. Answer "BANC".
5Approach steps
need = Counter(t),window = {},left = 0,count = len(t), best length = ∞.- For each right, c = s[right]:
window[c] += 1; if c in need and window[c] ≤ need[c] →count −= 1. - While
count == 0(valid): record the window if shorter; d = s[left]; if d in need and window[d] ≤ need[d] →count += 1;window[d] −= 1;left += 1. - Return the best window, or "".
6Code (Python)
from collections import Counter
class Solution:
def minWindow(self, s, t):
need = Counter(t) # tMap
window = {} # sMap: counts inside s[left..right]
count = len(t) # characters of t still missing
left = 0
best_len, best_left = float('inf'), 0
for right in range(len(s)):
c = s[right]
window[c] = window.get(c, 0) + 1 # right adds c
if c in need and window[c] <= need[c]: # a required copy arrived
count -= 1
while count == 0: # valid -> squeeze it
if right - left + 1 < best_len:
best_len, best_left = right - left + 1, left
d = s[left]
if d in need and window[d] <= need[d]: # losing a required copy
count += 1
window[d] -= 1 # left undoes right
left += 1
return "" if best_len == float('inf') else s[best_left:best_left + best_len]7Code line by line
| line | what it means |
|---|---|
| count = len(t) | Every character of t (each copy) is still missing at the start. |
| window[c] = window.get(c, 0) + 1 | Expansion: right always records the new character, needed or not. |
| if c in need and window[c] <= need[c]: | c is a character of t, and this copy is within what t asks for (not an extra) → one fewer missing. |
| while count == 0: | The window covers t. Keep shrinking while that stays true. |
| if right - left + 1 < best_len: | Record the current valid window if it's the smallest so far. |
| if d in need and window[d] <= need[d]: count += 1 | The leaving character was a required copy (no spare copy in the window) → something is missing again. This undoes the count −= 1 that right did. |
| window[d] -= 1; left += 1 | Shrinking: remove the left character and move left. |
| return … | Cut the answer once at the end; "" if nothing ever covered t. |
8Dry run (hand table)
s = "ADOBECODEBANC", t = "ABC". need = {A:1, B:1, C:1}, count starts at 3. "w" = the window map (only A, B, C shown; other letters are tracked too but never touch count).
| step | right (char added) | window before shrink | w[A,B,C] / count after adding | shrink? (what leaves, why) | window after | best |
|---|---|---|---|---|---|---|
| 1 | 0 'A' | A | 1,0,0 / 2 | no (count 2) | [0..0] | — |
| 2 | 1 'D' | AD | 1,0,0 / 2 | no | [0..1] | — |
| 3 | 2 'O' | ADO | 1,0,0 / 2 | no | [0..2] | — |
| 4 | 3 'B' | ADOB | 1,1,0 / 1 | no | [0..3] | — |
| 5 | 4 'E' | ADOBE | 1,1,0 / 1 | no | [0..4] | — |
| 6 | 5 'C' | ADOBEC | 1,1,1 / 0 | record 6; A leaves (required copy) → count 1 | [1..5] DOBEC | ADOBEC (6) |
| 7 | 6 'O' | DOBECO | 0,1,1 / 1 | no | [1..6] | 6 |
| 8 | 7 'D' | DOBECOD | 0,1,1 / 1 | no | [1..7] | 6 |
| 9 | 8 'E' | DOBECODE | 0,1,1 / 1 | no | [1..8] | 6 |
| 10 | 9 'B' | DOBECODEB | 0,2,1 / 1 (extra B, count unchanged) | no | [1..9] | 6 |
| 11 | 10 'A' | DOBECODEBA | 1,2,1 / 0 | record 10, D leaves; record 9, O leaves; record 8, B(3) leaves (spare, count stays 0); record 7, E leaves; record 6 (not shorter), C leaves (required) → count 1 | [6..10] ODEBA | 6 |
| 12 | 11 'N' | ODEBAN | 1,1,0 / 1 | no | [6..11] | 6 |
| 13 | 12 'C' | ODEBANC | 1,1,1 / 0 | record 7, O leaves; record 6, D leaves; record 5, E leaves; record 4, B leaves (required) → count 1 | [10..12] ANC | BANC (4) |
Final answer: "BANC" ✓
9Complexity & remember
- O(n) to build need from t.
rightwalks s once; it never comes back.leftalso only moves forward, at most m steps over the whole run. Thewhileinside thefordoes not multiply. Total O(m + m) = O(2m), plus O(n) → O(m + n).- Space: two maps, each at most 256 keys. Twice the space of Part D.
Part C · The same window with a formed / required counter
Not from the video. It's the other common way to write Part B's counter, and you'll meet it in editorials. Same window, same shrinking. Only the meaning of the counter changes.
1The idea in simple words
Instead of counting copies still missing, count distinct characters that are fully satisfied:
required= how many different characters t has. For t = "AABC" that's 3 (A, B, C), not 4.formed= how many of those characters currently have enough copies in the window (window[c] ≥ need[c]).- The window is valid exactly when
formed == required.
2When does formed change? Only at the crossing moment
Think of each character of t as a small progress bar that fills up to need[c]:
- Right adds c, and window[c] becomes exactly need[c] → the bar just filled →
formed += 1. - Left removes c, and window[c] drops to exactly need[c] − 1 → the bar just stopped being full →
formed −= 1.
== and not >= when adding?→ With
>=, every extra copy would add 1 again. In "ADOBECODEB", the second B would push formed up a second time for the same character, and the window would look valid without an A. That's the same bug as Part B's "naive rule". Using == counts each character once, at the exact moment it becomes satisfied. Removing works the same way: only the drop from "enough" to "one short" changes formed. Dropping from 3 spare copies to 2 doesn't.→ Part B: count = copies still missing, starts at len(t), valid at 0, and the check is "w[c] ≤ need[c]". Part C: formed goes up to required (distinct), and the check is "w[c] == need[c]". Same windows, same answer. formed/required just reads more naturally as "how many characters are done".
3Why we shrink while valid (the same reason, said once more)
While formed == required, the window covers t. Moving right would only make it longer, so the only way to find something shorter is to drop characters from the left, recording each valid size. As soon as a needed character falls below its count (formed < required), every smaller window ending at this right is missing it too, so we stop and expand right again. Because coverage is monotonic, left never has to go back.
4Code (Python)
from collections import Counter
class Solution:
def minWindow(self, s, t):
need = Counter(t)
required = len(need) # distinct characters t needs
window = {}
formed = 0 # distinct characters fully satisfied
left = 0
best_len, best_left = float('inf'), 0
for right in range(len(s)):
c = s[right]
window[c] = window.get(c, 0) + 1
if c in need and window[c] == need[c]: # c just became satisfied
formed += 1
while formed == required: # valid -> squeeze it
if right - left + 1 < best_len:
best_len, best_left = right - left + 1, left
d = s[left]
window[d] -= 1
if d in need and window[d] == need[d] - 1: # d just fell short
formed -= 1
left += 1
return "" if best_len == float('inf') else s[best_left:best_left + best_len]5Dry run with duplicates in t (hand table)
s = "AABXAB", t = "AAB" → need = {A:2, B:1}, required = 2. Here count-of-copies and formed differ visibly.
| step | right (char added) | window before shrink | w[A,B] / formed after adding | shrink? (what leaves, why) | window after | best |
|---|---|---|---|---|---|---|
| 1 | 0 'A' | A | 1,0 / 0 | no | [0..0] | — |
| 2 | 1 'A' | AA | 2,0 / 1 (A hit 2) | no | [0..1] | — |
| 3 | 2 'B' | AAB | 2,1 / 2 (B hit 1) | record 3; A leaves → A=1 = 2−1 → formed 1 | [1..2] AB | AAB (3) |
| 4 | 3 'X' | ABX | 1,1 / 1 | no | [1..3] | 3 |
| 5 | 4 'A' | ABXA | 2,1 / 2 | record 4 (no); A leaves → A=1 → formed 1 | [2..4] BXA | 3 |
| 6 | 5 'B' | BXAB | 1,2 / 1 (B was already full: 2 ≠ 1, no change) | no | [2..5] | 3 |
Answer "AAB" ✓. Step 6 shows an extra B that changes nothing, because B was already satisfied.
6Complexity & remember
Same as Part B: O(m + n) time, two maps of at most 256 keys.
Part D · Space-optimised: one map with negative counts
1What's the same and what changes
The window, the count (starts at len(t), valid at 0), and "shrink while valid" are all the same as Part B. The change: no separate window map. We keep only need, and let it go up and down.
The teacher's view: beginners naturally think of two maps first (that's why she taught Part B), and almost nobody comes up with the one-map version straight away. So learn it as a step after Part B.
2Intuition: need[c] = "how many more c's do I still want?"
- Start: need = counts of t. e.g. {A:1, B:1, C:1}.
- Right takes c:
need[c] −= 1for every character, even ones not in t. Before decreasing, if need[c] was > 0, this copy was really wanted →count −= 1. - So need[c] can go negative. For a character not in t, D → −1 means "one D in the window that nobody asked for". For B → −1, it means "one spare B".
- Left gives c back:
need[c] += 1. If after adding it's > 0, we just lost a wanted copy →count += 1.
In short: positive = still wanted, 0 = exactly satisfied, negative = spare copies. One number per character does the job of two maps.
→ In the one-map version, need[c] = t's count − window's count. "> 0 before taking this copy" means the window had fewer than t needs, so this copy fills a real gap. That's exactly "after adding, window[c] ≤ need[c]" in Part B.
→ No. They start at 0 (not in t), so they're never > 0 before right takes them, and they never go above 0 when left returns them. They never touch count. They just sit in the map. (In Python,
need.get(c, 0) or a Counter handles the "not there yet" case.)3Approach steps
need = Counter(t),count = len(t),left = 0, best length = ∞.- For each right, c = s[right]: if need[c] > 0 → count −= 1. Then need[c] −= 1.
- While count == 0: record if shorter; d = s[left]; need[d] += 1; if need[d] > 0 → count += 1; left += 1.
- Return the best window or "".
4Code (Python)
from collections import Counter
class Solution:
def minWindow(self, s, t):
need = Counter(t) # how many more of each char we want
count = len(t) # wanted copies still missing
left = 0
best_len, best_left = float('inf'), 0
for right in range(len(s)):
c = s[right]
if need[c] > 0: # this copy fills a real gap
count -= 1
need[c] -= 1 # right always takes one (may go negative)
while count == 0: # valid -> squeeze it
if right - left + 1 < best_len:
best_len, best_left = right - left + 1, left
d = s[left]
need[d] += 1 # left gives it back
if need[d] > 0: # now we're short of d again
count += 1
left += 1
return "" if best_len == float('inf') else s[best_left:best_left + best_len]5Code line by line
| line | what it means |
|---|---|
| need = Counter(t) | The only map. A Counter returns 0 for missing keys, so need[c] never crashes. |
| if need[c] > 0: count -= 1 | We still wanted a c, so this one counts toward t. |
| need[c] -= 1 | Right takes c into the window in every case. Negative = more than t asked for. |
| need[d] += 1 | Left undoes right's −1. |
| if need[d] > 0: count += 1 | After giving it back, we want a d again → the window lost a required copy → invalid. |
6Dry run (hand table)
s = "ADOBECODEBANC", t = "ABC". The map column shows need for A, B, C and the extra letters (D, O, E, N).
| step | right (char) | window before shrink | need after taking / count | shrink? (what leaves, why) | window after | best |
|---|---|---|---|---|---|---|
| 1 | 0 'A' | A | A0 B1 C1 / 2 | no | [0..0] | — |
| 2–3 | 'D','O' | ADO | D−1 O−1 / 2 | no | [0..2] | — |
| 4 | 3 'B' | ADOB | B0 / 1 | no | [0..3] | — |
| 5 | 4 'E' | ADOBE | E−1 / 1 | no | [0..4] | — |
| 6 | 5 'C' | ADOBEC | C0 / 0 | record 6; A back → A1 > 0 → count 1 | [1..5] | ADOBEC (6) |
| 7–9 | 'O','D','E' | DOBECODE | O−2 D−2 E−2 / 1 | no | [1..8] | 6 |
| 10 | 9 'B' | DOBECODEB | B was 0 (not > 0) → B−1 (spare) / 1 | no | [1..9] | 6 |
| 11 | 10 'A' | DOBECODEBA | A was 1 → A0 / 0 | D back (−1), O back (−1), B back (−1→0, not > 0), E back (−1), C back (0→1 > 0 → count 1) | [6..10] ODEBA | 6 |
| 12 | 11 'N' | ODEBAN | N−1 / 1 | no | [6..11] | 6 |
| 13 | 12 'C' | ODEBANC | C was 1 → C0 / 0 | record 7, O back (−1→0); record 6, D back (0); record 5, E back (0); record 4, B back (0→1 > 0) → count 1 | [10..12] ANC | BANC (4) |
Same steps and the same answer, "BANC" ✓, with one map. Look at step 10: B goes to −1, which says "one spare B", the same thing Part B tracked as window[B] = 2 > need 1.
7Complexity & remember
- Time O(m + n), the same as Part B (both pointers move forward only).
- Space: one map instead of two, half the memory. The time complexity doesn't change; only the space does.
Part E · Revision page
| Brute force | Two maps + count | Two maps + formed/required | One map | |
|---|---|---|---|---|
| valid when | covers() loop is True | count == 0 | formed == required | count == 0 |
| counter starts at | — | len(t) (copies) | 0, goal = distinct chars of t | len(t) |
| right changes counter when | — | c in t and w[c] ≤ need[c] | w[c] == need[c] | need[c] > 0 (before −1) |
| left changes counter when | — | d in t and w[d] ≤ need[d] (before −1) | w[d] == need[d] − 1 (after −1) | need[d] > 0 (after +1) |
| time | O(m²·256) → TLE | O(m + n) | O(m + n) | O(m + n) |
| space | 2 maps | 2 maps | 2 maps | 1 map |
| Longest valid window (problems 11, 12) | Shortest valid window (this one) | |
|---|---|---|
| shrink | while invalid, to repair | while valid, to squeeze |
| record | after the while | inside the while, before removing |
| starting answer | 0 or −1 | ∞ (and return "" if it stays ∞) |
2. Grow right until valid; then shrink while valid, recording each size.
3. A counter replaces the cover check: count of missing copies (→ 0) or formed vs required.
4. Only required copies touch the counter; extra copies don't.
5. One map works too: positive = wanted, 0 = exact, negative = spare.
✗
>= instead of == for formed (counts a char many times)✗ shrinking with
if, or only when invalid (misses smaller windows)✗ recording after removing the left char
✗ starting the answer as "" and comparing lengths (never updates)
✗ forgetting the "" case when t can't be covered (t longer than s, missing chars)
s = Solution()
print(repr(s.minWindow("ADOBECODEBANC", "ABC"))) # 'BANC'
print(repr(s.minWindow("a", "a"))) # 'a'
print(repr(s.minWindow("a", "aa"))) # ''
print(repr(s.minWindow("AABXAB", "AAB"))) # 'AAB'
print(repr(s.minWindow("ab", "b"))) # 'b'
print(repr(s.minWindow("abc", "cba"))) # 'abc'Based on this video: Minimum Window Substring | Brute Force, Two Maps & One Map