🎉 All 30 days are live — the full DSA-30 course, from Big-O to System Design. See the roadmap →
Day 24 - Sliding WindowThe At-Most-K Trick

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.

Predict first
Variable-window cleanly counts 'at most K distinct.' Why can't you slide a window for 'EXACTLY K distinct' the same direct way?

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 nums and integer k, return the number of good subarrays of nums. A good subarray is a contiguous subarray with exactly k distinct 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:

python · fill in the blanks0/2 hints
def at_most_k(nums, k):
freq = defaultdict(int)
ans = l = 0
for r in range(len(nums)):
freq[nums[r]] += 1
while len(freq) > k: # too many distinct -> shrink
freq[nums[l]] -= 1
if freq[nums[l]] == 0: del freq[nums[l]]
l += 1
# ??? count contribution / exactly-k subtraction
return ans
def subarrays_with_k_distinct(nums, k):
# ??? count contribution / 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 nums and an integer k. A continuous subarray is called nice if there are k odd 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 nums and integer goal, return the number of non-empty subarrays with sum equal to goal.

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:

  1. The problem asks for “exactly K” of some countable property over a contiguous range.
  2. 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.)
  3. 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

Why does atMost(k) - atMost(k - 1) equal 'exactly k'?
What property must hold for the at-most-K trick to work?
For Subarrays with K Different Integers using atMost(k) - atMost(k - 1), what's the total time complexity?

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.

Finished this page?