Longest Substring with At Most K Distinct Characters
Return the length of the longest substring containing at most k distinct characters.
Open on LeetCode ↗Intuition
Same window template as the binary-flips problem, with the constraint upgraded from a counter to a frequency map. The window is legal while the map holds at most k keys; when a k+1th key appears, shrink from the left and delete keys as their counts hit zero. The trap is skipping the delete: if a count hits zero but the key stays in freq, len(freq) keeps counting a character that is no longer in the window, so the shrink condition never clears and the window gets squeezed down to nothing.
The canonical k-distinct window, and the parent of several disguised problems (fruit baskets is this with k = 2). A frequency map tracks the window's contents; len(freq) is the validity test; deleting keys at zero is what keeps that test honest.
Approach
Define the constraint as map size
Keep a dictionary from character to count for the current window. The number of distinct characters is exactly len(freq), so the legality test is len(freq) <= k. The brute force enumerates all O(n^2) substrings and builds a set for each — the window reuses that work instead.
Grow right and add to the map
For each right, do freq[s[right]] += 1. A brand-new character raises the distinct count by one, which is the only way the window can become illegal — so the shrink check only needs to run after an insert.
Shrink and delete zeroed keys
While len(freq) > k, decrement freq[s[left]] and advance left, and delete the key when its count reaches zero. Forgetting the delete is the standard bug: the map keeps a stale key, len(freq) never drops, and the loop shrinks the window to nothing. After shrinking, update the best length. O(n) time, O(k) space for the map.
Solution & live demo
Common pitfalls
Keeping keys after their count reaches zero
freq[out] -= 1 left += 1
freq[out] -= 1
if freq[out] == 0:
del freq[out]
left += 1len(freq) counts keys, not positive counts. A key stranded at zero makes the window look more diverse than it is, so it shrinks past the real answer.
Testing distinctness against the window length
while right - left + 1 > k:
while len(freq) > k:
The constraint bounds distinct characters, not the substring's length. Bounding the length caps every answer at k and ignores repeats entirely.
Not handling k = 0
# assume k >= 1
while len(freq) > k: # naturally empties the window when k == 0
With k = 0 the only valid substring is empty. The while loop handles it correctly by shrinking left up to right + 1; an if-based shrink would leave a one-character window and return 1.
Edge cases
No characters are allowed, so the answer is 0.
The whole string qualifies and the answer is len(s).
len(freq) overstates the distinct count and the window collapses — the most common failure here.
The loop never runs and 0 is returned.