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 · 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

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-sizeVariable-size
whenlength 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 forshrink when…record the answer…
longest validthe 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 validinvalidcount += 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:

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.

variable-size "shortest valid window" template (Part 0 helper)
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 "".

index0123456789101112
sADOBECODEBANCt = "ABC" → "BANC"

"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

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.

Doubt 1: once "ADOBEC" is valid, should j keep going?
→ 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.

Doubt 2: B appears twice but t has only one B. Is that still valid?
→ 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

  1. Build need = Counter(t).
  2. For each start i: empty window map.
  3. For j from i: add s[j] to the window.
  4. If the window covers need: if j − i + 1 is shorter than the best, save it. Break.
  5. Return the best substring, or "".

6Code (Python)

Brute force: every start + window map + cover check
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 True

7Code line by line

linewhat 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]] = … + 1Grow 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.
breakFor 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.
Doubt 3: she stores the substring itself each time. Why do we store start + length?
→ 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".

ifirst j where the window covers twindowlengthbest so far
05ADOBEC6ADOBEC (6)
110DOBECODEBA106
210OBECODEBA96
310BECODEBA86
410ECODEBA76
510CODEBA6 (not shorter)6
612ODEBANC76
712DEBANC66
812EBANC5EBANC (5)
912BANC4BANC (4)
10–12never (no B after index 9)——4

Final answer: "BANC" ✓

9Complexity & remember

Remember the brute forcetMap from t. For each start: new window map, grow until it covers t ("at least" for every char), keep it if shorter, then break.

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

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.

The real rule (frequency matters) Right adds c: 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:

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.

Doubt 1: why record the answer before removing the left character?
→ 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.
Doubt 2: why does 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

  1. need = Counter(t), window = {}, left = 0, count = len(t), best length = ∞.
  2. For each right, c = s[right]: window[c] += 1; if c in need and window[c] ≤ need[c] → count −= 1.
  3. 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.
  4. Return the best window, or "".

6Code (Python)

Two maps + count of still-needed characters, O(m + n)
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

linewhat it means
count = len(t)Every character of t (each copy) is still missing at the start.
window[c] = window.get(c, 0) + 1Expansion: 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 += 1The 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 += 1Shrinking: 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).

stepright (char added)window before shrinkw[A,B,C] / count after addingshrink? (what leaves, why)window afterbest
10 'A'A1,0,0 / 2no (count 2)[0..0]—
21 'D'AD1,0,0 / 2no[0..1]—
32 'O'ADO1,0,0 / 2no[0..2]—
43 'B'ADOB1,1,0 / 1no[0..3]—
54 'E'ADOBE1,1,0 / 1no[0..4]—
65 'C'ADOBEC1,1,1 / 0record 6; A leaves (required copy) → count 1[1..5] DOBECADOBEC (6)
76 'O'DOBECO0,1,1 / 1no[1..6]6
87 'D'DOBECOD0,1,1 / 1no[1..7]6
98 'E'DOBECODE0,1,1 / 1no[1..8]6
109 'B'DOBECODEB0,2,1 / 1 (extra B, count unchanged)no[1..9]6
1110 'A'DOBECODEBA1,2,1 / 0record 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] ODEBA6
1211 'N'ODEBAN1,1,0 / 1no[6..11]6
1312 'C'ODEBANC1,1,1 / 0record 7, O leaves; record 6, D leaves; record 5, E leaves; record 4, B leaves (required) → count 1[10..12] ANCBANC (4)
step 6ADOBECODEBANCfirst valid window, 6
L    R       
step 11ADOBECODEBANCvalid again; left will squeeze D, O, B, E away
 L        R  
step 13ADOBECODEBANCsmallest valid window: BANC
         L  R

Final answer: "BANC" ✓

9Complexity & remember

Remember the two-map windowcount = len(t). Right: w[c] += 1, and if c is needed and w[c] ≤ need[c], count −= 1. While count == 0: record, then if the left char is a required copy, count += 1; w −= 1; left += 1.

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:

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]:

Doubt 1: why == 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.
Doubt 2: how is this different from Part B's count?
→ 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)

Two maps + formed / required counter
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.

stepright (char added)window before shrinkw[A,B] / formed after addingshrink? (what leaves, why)window afterbest
10 'A'A1,0 / 0no[0..0]—
21 'A'AA2,0 / 1 (A hit 2)no[0..1]—
32 'B'AAB2,1 / 2 (B hit 1)record 3; A leaves → A=1 = 2−1 → formed 1[1..2] ABAAB (3)
43 'X'ABX1,1 / 1no[1..3]3
54 'A'ABXA2,1 / 2record 4 (no); A leaves → A=1 → formed 1[2..4] BXA3
65 'B'BXAB1,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.

Remember formed / requiredrequired = distinct chars of t. formed +1 when w[c] reaches need[c]; −1 when it drops to need[c] − 1. Valid ⇔ formed == required. Shrink while valid, recording each time.

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?"

In short: positive = still wanted, 0 = exactly satisfied, negative = spare copies. One number per character does the job of two maps.

Doubt 1: why is "need[c] > 0 before decrementing" the same as Part B's "window[c] ≤ need[c]"?
→ 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.
Doubt 2: characters not in t also go into the map with negative values. Is that a problem?
→ 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

  1. need = Counter(t), count = len(t), left = 0, best length = ∞.
  2. For each right, c = s[right]: if need[c] > 0 → count −= 1. Then need[c] −= 1.
  3. While count == 0: record if shorter; d = s[left]; need[d] += 1; if need[d] > 0 → count += 1; left += 1.
  4. Return the best window or "".

4Code (Python)

One map: need counts go negative, O(m + n)
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

linewhat 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 -= 1We still wanted a c, so this one counts toward t.
need[c] -= 1Right takes c into the window in every case. Negative = more than t asked for.
need[d] += 1Left undoes right's −1.
if need[d] > 0: count += 1After 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).

stepright (char)window before shrinkneed after taking / countshrink? (what leaves, why)window afterbest
10 'A'AA0 B1 C1 / 2no[0..0]—
2–3'D','O'ADOD−1 O−1 / 2no[0..2]—
43 'B'ADOBB0 / 1no[0..3]—
54 'E'ADOBEE−1 / 1no[0..4]—
65 'C'ADOBECC0 / 0record 6; A back → A1 > 0 → count 1[1..5]ADOBEC (6)
7–9'O','D','E'DOBECODEO−2 D−2 E−2 / 1no[1..8]6
109 'B'DOBECODEBB was 0 (not > 0) → B−1 (spare) / 1no[1..9]6
1110 'A'DOBECODEBAA was 1 → A0 / 0D back (−1), O back (−1), B back (−1→0, not > 0), E back (−1), C back (0→1 > 0 → count 1)[6..10] ODEBA6
1211 'N'ODEBANN−1 / 1no[6..11]6
1312 'C'ODEBANCC was 1 → C0 / 0record 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] ANCBANC (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

Remember the one-map versionneed[c] > 0 → count −= 1; need[c] −= 1 always. While count == 0: record; need[d] += 1; if need[d] > 0 → count += 1; left += 1. Positive = wanted, 0 = exact, negative = spare.

Part E · Revision page

Brute forceTwo maps + countTwo maps + formed/requiredOne map
valid whencovers() loop is Truecount == 0formed == requiredcount == 0
counter starts at—len(t) (copies)0, goal = distinct chars of tlen(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)
timeO(m²·256) → TLEO(m + n)O(m + n)O(m + n)
space2 maps2 maps2 maps1 map
Longest valid window (problems 11, 12)Shortest valid window (this one)
shrinkwhile invalid, to repairwhile valid, to squeeze
recordafter the whileinside the while, before removing
starting answer0 or −1∞ (and return "" if it stays ∞)
If you remember only 5 lines 1. Valid = window has at least t's count of every character.
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.
Mistakes to avoid ✗ decrementing count for an extra copy (window looks valid while missing a char)
✗ >= 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)
test it yourself (paste under any Solution above)
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