Search Tech Journey

Find topics, journeys and posts

back to blog
algorithmsintermediate 60m read

L11 · Sliding Window III — With Counts

Windows whose validity depends on a frequency map, and the single integer counter that lets you test that condition in O(1) instead of rescanning the map.

🧩DSAPhase 1 · Core patterns· Session 011 of 130 60 min

🎯 Own count-driven windows: a need-map plus one `formed` integer that tells you the window is valid in O(1), the technique behind Minimum Window Substring.

Series: LeetCode — From Basics to Interview-Ready · Session 11 / 65 · Phase 1 · Core patterns

Watch first

What this pattern IS

L10's windows were valid by a simple test: no repeats (a set), sum ≥ target (an integer). But some windows are valid only when they contain enough of each of several characters — "does this window contain every letter of t with at least the required multiplicity?"

The naive validity test rebuilds or compares a whole frequency map on every step. Comparing two maps is O(alphabet); doing it inside a window loop makes the constant fat and, for large alphabets, genuinely slow.

The count technique compresses that whole comparison into one integer. You keep:

  • need — a Counter of how many of each character the window must contain.
  • have — how many characters currently meet their required count.

When have == len(need), every required character is satisfied — the window is valid — and you learned that in O(1), by watching a single counter tick up and down. That's the entire trick, and it's what turns Minimum Window Substring from scary into routine.

A checklist with a completed-count
🌍 Real world
A shipping order needs 3 red, 2 blue, 1 green. Instead of re-reading the whole checklist each time an item arrives, you keep one number: how many line-items are fully satisfied. When that number equals the number of line-items, the order is complete.
💻 Code world
need = {'r':3,'b':2,'g':1}; when window[c] == need[c], have += 1; valid when have == len(need).

First principles

From first principles
Start with the question
Why can 'window contains enough of every required char' be tested in O(1) instead of comparing two maps?
  1. 1
    Validity = for every required char c, window_count[c] ≥ need[c]. That's a conjunction over all required chars.
    forced by · The window is valid exactly when EVERY line-item is met — an AND across the need-map keys.
  2. 2
    A conjunction of booleans equals 'number of true clauses == total clauses'.
    forced by · All clauses true ⇔ the count of true clauses reaches the total count.
  3. 3
    That count changes by at most 1 when a single character enters or leaves the window.
    forced by · Adding one char can push exactly one line-item from unmet to met; removing one can push exactly one from met to unmet.
  4. 4
    So maintain 'have = number of satisfied line-items' incrementally, adjusting by ±1 on each add/remove.
    forced by · You never rescan the map — you only react to the one char that moved across the window edge.
  5. 5
    Validity is then the O(1) test have == len(need).
    forced by · One integer comparison replaces an O(alphabet) map comparison.
⇒ Therefore
Track a single 'satisfied line-items' counter; the window is valid iff it equals the number of required characters.

The templates

Template A — minimum window covering a target multiset

from collections import Counter
 
def min_window(s, t):
    need = Counter(t)              # required counts
    missing = len(need)           # number of DISTINCT chars not yet satisfied
    window = Counter()
    left = 0
    best = (float('inf'), 0, 0)   # (length, l, r)
    for right, ch in enumerate(s):
        window[ch] += 1
        if ch in need and window[ch] == need[ch]:
            missing -= 1          # this char just became fully satisfied
        while missing == 0:       # window is valid -> shrink to minimise
            if right - left + 1 < best[0]:
                best = (right - left + 1, left, right)
            lc = s[left]
            window[lc] -= 1
            if lc in need and window[lc] < need[lc]:
                missing += 1      # dropped below requirement -> no longer satisfied
            left += 1
    return "" if best[0] == float('inf') else s[best[1]:best[2] + 1]

The two if ch in need and window[ch] == need[ch] / < need[ch] lines are the whole O(1) validity engine. Note the strict == on the way up (fires exactly once, when you first hit the requirement) and < on the way down (fires exactly once, when you first drop below it).

Template B — fixed window with a single matches counter (anagram / permutation)

from collections import Counter
 
def contains_permutation(s1, s2):
    need = Counter(s1)
    k = len(s1)
    window = Counter()
    matches = 0                   # how many chars have window[c] == need[c]
    for right, ch in enumerate(s2):
        window[ch] += 1
        if window[ch] == need[ch]:
            matches += 1
        elif window[ch] == need[ch] + 1:
            matches -= 1          # just overshot -> no longer a match
        if right >= k:            # drop the char leaving the fixed window
            lc = s2[right - k]
            if window[lc] == need[lc]:
                matches -= 1
            elif window[lc] == need[lc] + 1:
                matches += 1      # came back down to a match
            window[lc] -= 1
        if matches == len(need):  # every char count matches exactly
            return True
    return False

Here matches tracks exact equality (permutation needs equal counts, not "at least"), so both overshoot and undershoot flip it.

Mental model

Mental modelOne counter, not a whole map
A dashboard with a single gauge: 'requirements met'. Each character crossing the window edge nudges the gauge by at most ±1. Window valid ⇔ gauge is full. Never re-read the whole map.
  • Keep need (targets) and window (live counts).
  • On entering char: if it just hit its target, satisfied += 1.
  • On leaving char: if it just dropped below target, satisfied -= 1.
  • Valid ⇔ satisfied == number of distinct required chars.
🔔 Fires when you see
'window/substring containing all of ...', 'anagram/permutation', 'at least/exactly the required count of each character'.

Memory / mnemonic

The engine is four guarded lines. Peg them as "UP on equal, DOWN on below."

  • Entering a char: it can only raise your window count, so the only interesting crossing is window[c] == need[c] → satisfied UP.
  • Leaving a char: it can only lower the count, so the interesting crossing is window[c] < need[c] → satisfied DOWN.

For the exact-match (permutation) variant, add the overshoot beat: "equal is a match, one-over breaks it." == need[c] gains a match; == need[c] + 1 loses it.

The recall cue for the whole family: "need-map + one integer." If your validity check loops over the map, you've forgotten the integer.

The trap

Common misconception
✗ What most people think
To check window validity you compare the window's Counter to the need Counter each step.
✓ What is actually true
That map comparison is O(alphabet) per step and unnecessary. Maintain a single `satisfied`/`missing` integer that moves by ±1 as chars cross the edges.
Why the myth is so sticky
Only one character enters or leaves per step, so at most one requirement changes status — a full comparison redoes work for every other unchanged key.
Prove it to yourself
On a large alphabet or long string, the map-comparison version is measurably slower and often TLEs on Minimum Window Substring's tight cases; the counter version passes comfortably.

What interviewers actually ask

Count-window problems are a favourite because Minimum Window Substring is a genuine filter and its lighter cousins are extremely common.

  • Minimum Window Substring (76) · Hard · Meta, Amazon, Uber — probes: the need-map + satisfied-counter with correct shrink; the definitive test of this pattern. Follow-up: what if there are multiple minimum windows, or the input streams?
  • Find All Anagrams in a String (438) · Medium · Meta, Amazon — probes: fixed window with a matches counter; return all start indices. Follow-up: count matches only, or a huge alphabet.
  • Permutation in String (567) · Medium · Meta, Amazon — probes: exact-count fixed window (overshoot breaks a match). Follow-up: return the first index, or handle repeats in s1.
  • Longest Substring with At Most K Distinct Characters (340) · Medium · Amazon, Meta — probes: distinct-count via len(window) as the validity signal. Follow-up: exactly K distinct (subtract two at-most bounds).
  • Substring with Concatenation of All Words (30) · Hard · Amazon — probes: word-level count window with multiple offsets. Follow-up: variable word lengths.
  • Longest Repeating Character Replacement (424) · Medium · Google, Amazon — probes: validity via maxfreq (a count-derived signal). Follow-up: why maxfreq needn't decrease.

Universal probe: "don't rescan the map — keep it O(1) per step." Saying and coding that is the pass.

Worked solutions

1. Minimum Window Substring (76)

Plain words: given strings s and t, return the shortest substring of s that contains every character of t including duplicates, or "" if none exists.

Brute force: check every substring for coverage — O(n^2) windows times O(alphabet) coverage = far too slow. Insight: variable window (L10) + the count engine — grow to cover t, then shrink to minimise, testing validity with the missing integer.

from collections import Counter
 
def minWindow(s, t):
    if not t or not s:
        return ""
    need = Counter(t)
    missing = len(need)               # distinct chars still under their requirement
    window = Counter()
    left = 0
    best = (float('inf'), 0, 0)
    for right, ch in enumerate(s):
        window[ch] += 1
        if ch in need and window[ch] == need[ch]:
            missing -= 1              # ch is now fully covered
        while missing == 0:          # valid -> record and shrink
            if right - left + 1 < best[0]:
                best = (right - left + 1, left, right)
            lc = s[left]
            window[lc] -= 1
            if lc in need and window[lc] < need[lc]:
                missing += 1         # dropped below -> window no longer valid
            left += 1
    return "" if best[0] == float('inf') else s[best[1]:best[2] + 1]

Complexity: O(|s| + |t|) time, O(alphabet) memory. Follow-up — "streaming s": the algorithm is already single-pass over s with two forward pointers, so it streams as long as you can buffer the current window.

Try itReturn the START INDEX of the minimum window instead of the substring.
def minWindowStart(s, t):
    need = Counter(t); missing = len(need); window = Counter()
    left = 0; best = (float('inf'), -1)
    for right, ch in enumerate(s):
        window[ch] += 1
        if ch in need and window[ch] == need[ch]:
            missing -= 1
        while missing == 0:
            if right - left + 1 < best[0]:
                best = (right - left + 1, left)
            lc = s[left]; window[lc] -= 1
            if lc in need and window[lc] < need[lc]:
                missing += 1
            left += 1
    return best[1]
💡 Hint · best already stores (length, left, right); return best[1] when found, else -1.

2. Permutation in String (567)

Plain words: given s1 and s2, return True if any substring of s2 is a permutation of s1 (a window of length len(s1) with identical character counts).

Insight: fixed window of size len(s1); maintain a matches counter tracking how many characters have exactly the required count. Valid when matches == len(need).

from collections import Counter
 
def checkInclusion(s1, s2):
    if len(s1) > len(s2):
        return False
    need = Counter(s1)
    k = len(s1)
    window = Counter()
    matches = 0
    for right, ch in enumerate(s2):
        window[ch] += 1
        if window[ch] == need[ch]:
            matches += 1
        elif window[ch] == need[ch] + 1:
            matches -= 1              # overshot the exact count
        if right >= k:
            lc = s2[right - k]
            if window[lc] == need[lc]:
                matches -= 1
            elif window[lc] == need[lc] + 1:
                matches += 1
            window[lc] -= 1
        if matches == len(need):
            return True
    return False

Complexity: O(|s2|) time, O(26) memory. Follow-up — "return the first start index": return right - k + 1 at the match instead of True.

3. Longest Substring with At Most K Distinct Characters (340)

Plain words: return the length of the longest substring of s containing at most K distinct characters.

Insight: the validity signal is simply len(window) — the number of distinct chars currently in the window. Grow right; while more than K distinct, shrink left (deleting zero-count keys keeps len(window) honest).

from collections import defaultdict
 
def lengthOfLongestSubstringKDistinct(s, k):
    if k == 0:
        return 0
    window = defaultdict(int)
    left = 0
    best = 0
    for right, ch in enumerate(s):
        window[ch] += 1
        while len(window) > k:        # too many distinct -> shrink
            lc = s[left]
            window[lc] -= 1
            if window[lc] == 0:
                del window[lc]         # keep len(window) == true distinct count
            left += 1
        best = max(best, right - left + 1)
    return best

Complexity: O(n) time, O(k) memory. Follow-up — "exactly K distinct": compute atMost(K) − atMost(K−1) using this as a subroutine.

Tradeoff

The tradeoff
Test whether a window meets a multi-character count requirement.
Compare full Counters each step
+ you gain Very easy to write and reason about; hard to get wrong.
− you pay O(alphabet) per step; fat constant, can TLE on tight Hard cases.
pick when Small alphabet, short strings, or a first correct-but-slow pass.
Single satisfied/missing counter
+ you gain O(1) validity check; the expected optimal for Minimum Window Substring.
− you pay Fiddly ± bookkeeping at the exact ==/< crossings; easy to introduce an off-by-one.
pick when Whenever performance matters or the interviewer asks for O(1) per step.
Array-of-256 counts + matches
+ you gain Fastest constant; no hashing, cache-friendly.
− you pay Assumes a bounded alphabet; more code.
pick when Competitive settings or when the alphabet is known and small (ASCII).
What a senior engineer actually does
Lead with the single-counter version in interviews. Fall back to full-map comparison only to get a correct baseline fast, then optimise.

Where this shows up

Count-driven windows appear in log and stream monitoring: "alert when, within any rolling window, we've seen at least N of each critical error type" is exactly a need-map plus a satisfied-counter over a sliding time window. Bioinformatics uses the same fixed-window count-match to scan a genome for regions matching a required nucleotide composition — a direct analogue of Find All Anagrams over a 4-letter alphabet.

Checkpoint

You can now…
  • Explain why a multi-char validity condition compresses to a single integer.
  • Write the need-map + missing-counter Minimum Window Substring from scratch.
  • Get the ==/< crossings right so the counter moves by exactly ±1.
  • Use a matches counter for exact-count anagram/permutation windows.
  • Use len(window) as the validity signal for K-distinct problems.
  • Choose between full-map comparison and the O(1) counter based on constraints.

Quiz

Recall check · click to reveal
★ = stretch question

Practice queue

+7d / +21d mark spaced-repetition anchors.