Subarrays with K Different Integers is the problem that teaches a beautiful counting trick: when "exactly K" is hard, count "at most K" twice and subtract. It turns a tricky window into a routine one.
Problem. Given an integer array nums and an integer k, return the number of subarrays
(contiguous) that contain exactly k different integers.
Example: nums = [1, 2, 1, 3], k = 2 → answer 4. The good subarrays are [1, 2], [2, 1],
[1, 2, 1], and [1, 3] — each has exactly 2 distinct values.
The slow way first
The obvious idea: enumerate every subarray, drop each into a set, and check whether the set size is exactly k. There are O(n²) subarrays and building the set costs more on top, so this is O(n²) or worse — far too slow for a large array.
The natural instinct is a sliding window. But "exactly k distinct" does not slide cleanly: when you add an element the distinct count can jump, and there is no single window length that captures every valid subarray ending at a position. We need a cleaner quantity to count.
The idea: count "at most", then subtract
The fix is a classic identity:
exactly(k) = atMost(k) − atMost(k − 1)
A subarray with at most k distinct values, minus those with at most k − 1, leaves exactly those with exactly k. And atMost(k) does slide nicely: grow the window with hi; whenever the distinct count exceeds k, shrink from lo until it is valid again. At every hi, the number of valid subarrays ending there is the window length hi − lo + 1.
The key insight: counting hi − lo + 1 at each step adds every subarray that ends at hi and starts anywhere in the current window. Summed over all hi, that totals every valid subarray exactly once.
Walk through it
Step through the animation for atMost(2) on [1, 2, 1, 3]. The hi pointer grows the window; lo only moves when the distinct count exceeds 2. After each valid step we add the window length to count. The window stays valid through [1, 2, 1], then 3 pushes distinct to 3 and lo slides until we are back to [1, 3]. The running total reaches atMost(2) = 8. Doing the same with k = 1 gives atMost(1) = 4, so the answer is 8 − 4 = 4.
Pseudocode
function atMost(nums, k):
count = 0, lo = 0, freq = empty map
for hi from 0 to n-1:
add nums[hi] to freq
while freq has more than k distinct keys:
remove nums[lo] from freq (delete key if its count hits 0)
lo += 1
count += hi - lo + 1 # subarrays ending at hi
return count
answer = atMost(nums, k) - atMost(nums, k - 1)The Python solution
def at_most(nums, k):
count, lo, freq = 0, 0, {}
for hi, x in enumerate(nums):
freq[x] = freq.get(x, 0) + 1
while len(freq) > k:
left = nums[lo]
freq[left] -= 1
if freq[left] == 0: del freq[left]
lo += 1
count += hi - lo + 1
return count
def subarrays_k_distinct(nums, k):
return at_most(nums, k) - at_most(nums, k - 1)freqis a dictionary mapping each value in the window to how many times it appears.len(freq)is the distinct count.- We grow the window by adding
nums[hi]tofreq. - The
whileshrinks fromlowhenever distinct exceedsk. We decrement the leftmost value and delete the key when its count hits 0 — that is what actually drops the distinct count. - Line 10 is the heart of the count:
hi − lo + 1is the number of valid subarrays ending athi. - Line 14 applies the identity: the two
at_mostcalls share one helper, so we write the window logic once.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (every subarray) | O(n²) (slow) | build a set per subarray |
| atMost twice (this solution) | O(n) (moderate) | each index enters and leaves the window once |
O(k) (moderate)Each at_most call is O(n) because hi and lo each advance at most n times total, and we call it twice. The map holds at most k + 1 distinct keys, so extra space is O(k).
When this pattern shows up
Whenever a problem says "exactly K" of something countable — exactly K distinct, exactly K odd numbers, sum exactly K — try rewriting it as atMost(K) − atMost(K − 1). The "at most" version almost always slides cleanly even when "exactly" does not.
Two traps: when shrinking, you must delete the key once its frequency hits 0, or len(freq) will
overcount distinct values forever. And do not forget the k − 1 call can hit k = 0, which correctly
returns 0 — the helper handles it without a special case.
Practice
For nums = [1, 2, 1, 3] with k = 2, the animation shows atMost(2) = 8. What is atMost(1), and what is the final answer?
1. Why count atMost(k) − atMost(k − 1) instead of counting exactly k directly?
2. At each index hi, how many valid subarrays end there in the atMost helper?
3. Why must we delete a key from freq when its count reaches 0?
4. What is the time complexity of the full solution?