The At-Most-K Trick
How many subarrays have exactly K distinct integers? Sliding window is great at “at most K” — shrink whenever you exceed it — but “exactly K” has no clean expand/shrink rule. The fix is a sleight of hand worth memorizing:
exactly K = atMost(K) − atMost(K−1). Two easy windows, one subtraction, and the awkward problem evaporates.
Some problems ask for exactly a property — exactly K distinct integers, exactly K odd numbers, exactly K vowels. Sliding window doesn’t naturally count “exactly” — the window can drift past the target and back. But there’s a clean indirect move:
Count of subarrays with exactly K = Count with at most K − Count with at most (K − 1).
The “at most” version is a textbook variable-window problem. So you solve the same template twice with different k, subtract, done.
This trick is so common in interviews that recognizing it is half the battle.
Why does it work?
Define atMost(k) = number of contiguous subarrays whose property count is <= k.
Then:
atMost(K)= subarrays with property count in{0, 1, 2, ..., K}.atMost(K - 1)= subarrays with property count in{0, 1, 2, ..., K - 1}.- Subtraction = subarrays with property count exactly
K.
It’s set subtraction. The “at most” function is monotone (the answer for k is always >= the answer for k - 1), so the subtraction is always non-negative.
Worked example 1: Subarrays with K Different Integers
Given an integer array
numsand integerk, return the number of good subarrays ofnums. A good subarray is a contiguous subarray with exactlykdistinct integers.
Direct sliding window for “exactly k” is awkward — you’d have to enumerate every (l, r) and count. But atMost(k) is a standard variable-window count.
Step 1 — write atMost(k)
State: frequency map. Invalid when: len(map) > k. Count: r − l + 1 for each r once the window is valid.
Recall the two lines that are the trick — the per-r count contribution, and the exactly-K subtraction:
Step 2 — return the difference
atMostK(k) − atMostK(k - 1). That’s it.
Time: O(n) (two passes). Space: O(k).
Worked example 2: Count Number of Nice Subarrays
Given an array of integers
numsand an integerk. A continuous subarray is called nice if there arekodd numbers in it. Return the number of nice subarrays.
Same shape — “exactly k odd numbers.” Solve as atMost(k) − atMost(k - 1), where “at most k odd” is the windowed count.
Same atMost template, different running statistic (count of odd numbers instead of distinct-integer count). The two-call pattern is identical.
Worked example 3: Binary Subarrays With Sum
Given a binary array
numsand integergoal, return the number of non-empty subarrays with sum equal togoal.
Same trick: “exactly goal ones in the window.”
The if S < 0: return 0 guard handles the edge case where goal == 0 and we call at_most(-1).
When the trick applies
The at-most-K trick works whenever:
- The problem asks for “exactly K” of some countable property over a contiguous range.
- The property’s count over a window is monotone in window growth — adding an element can only increase or preserve the count, never decrease it. (Distinct values, odd numbers, sum of non-negative numbers, character frequencies — all monotone. Things like “max minus min” or signed sums are not.)
- The
atMost(k)version of the problem is a clean variable-size sliding window.
If all three hold, the trick is a near-immediate O(n) solution.
When the trick doesn’t apply
It fails when:
- The property isn’t monotone (signed sums, max − min with mixed values).
- The “at most” version isn’t easily slidable (e.g., the count depends on the window’s order, not just its multiset).
- You’re asked for “exactly K” of something where K = 0 is the natural empty answer (e.g., “no duplicates”); the trick still works but is overkill.
A cousin: prefix-sum + hash map
For “subarrays with sum equal to K” on signed integers (where sliding window doesn’t work because adding can decrease the sum), the standard fallback is prefix sums + hash map, which is O(n) time and O(n) space.
The at-most-K trick is the right tool for non-negative or counting properties. The prefix-sum trick is the right tool when the underlying quantity can decrease as the window grows. Know both.
The pattern in code, one more time
The reusable template:
def exactly_k_subarrays(nums, k, property_count):
"""
property_count(nums, K) = # subarrays where (some property count) <= K.
Returns: # subarrays where the property count is EXACTLY k.
"""
return property_count(nums, k) - property_count(nums, k - 1)For any problem where the property count is monotone over window growth, write at_most(K) as a 10-line variable-size sliding window, then return the subtraction. Two lines of “main” code, one slid template.
Quick check
The at-most-K trick is the slickest indirect move in the playbook — recognizing it is most of the battle. Next: advanced patterns — monotonic-deque windows (sliding-window maximum), and the string-frequency-matching variants that push the technique further.