Count runs with exactly k distinct values
Given an array nums and an integer k, return the number of contiguous
subarrays that contain exactly k distinct values.
Examples
| Input | Output | Why |
|---|---|---|
nums = [1,2,1,2,3], k = 2 | 7 | [1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2]. |
nums = [1,2,1,3,4], k = 3 | 3 | [1,2,1,3], [2,1,3], [1,3,4]. |
nums = [3,3,3], k = 2 | 0 |
Constraints
- 1 ≤ n ≤ 2 × 10⁴
- 1 ≤ k ≤ n
- 1 ≤ nums[i] ≤ n
Hints
Hint 1
Try the usual window: shrink while there are more than k values. Why does adding hi − lo + 1 overcount?
Hint 2
"At most k distinct" survives shrinking, so it can be counted with one window. Can you express "exactly k" through it?
Hint 3
Exactly k = (at most k) − (at most k − 1). Write atMost once and call it twice.
Where people lose marks
- Using one window and adding hi − lo + 1: that counts at most k, not exactly k.
- atMost(0) must return 0; write the function so the empty window is handled without a special case.
Dry run on [1,2,1,2,3],2
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 11nums10lo/hi21122334atMost(2)1
At most 2: the window 0..0 has 1 value; add 1 subarrays ending at 0. Running total 1.
-
Step 2 / 11nums10lo21hi122334atMost(2)3
At most 2: the window 0..1 has 2 values; add 2 subarrays ending at 1. Running total 3.
-
Step 3 / 11nums10lo2112hi2334atMost(2)6
At most 2: the window 0..2 has 2 values; add 3 subarrays ending at 2. Running total 6.
-
Step 4 / 11nums10lo211223hi34atMost(2)10
At most 2: the window 0..3 has 2 values; add 4 subarrays ending at 3. Running total 10.
-
Step 5 / 11nums10211223lo34hiatMost(2)12
At most 2: the window 3..4 has 2 values; add 2 subarrays ending at 4. Running total 12.
-
Step 6 / 11nums10lo/hi21122334atMost(1)1
At most 1: the window 0..0 has 1 value; add 1 subarrays ending at 0. Running total 1.
-
Step 7 / 11nums1021lo/hi122334atMost(1)2
At most 1: the window 1..1 has 1 value; add 1 subarrays ending at 1. Running total 2.
-
Step 8 / 11nums102112lo/hi2334atMost(1)3
At most 1: the window 2..2 has 1 value; add 1 subarrays ending at 2. Running total 3.
-
Step 9 / 11nums10211223lo/hi34atMost(1)4
At most 1: the window 3..3 has 1 value; add 1 subarrays ending at 3. Running total 4.
-
Step 10 / 11nums1021122334lo/hiatMost(1)5
At most 1: the window 4..4 has 1 value; add 1 subarrays ending at 4. Running total 5.
-
Step 11 / 11answer7
Exactly 2 = at most 2 − at most 1 = 12 − 5 = 7.
How to think about it
"Exactly k distinct" fails the test a window needs: removing an element from a valid subarray can make it invalid (k − 1 values), and adding one can too (k + 1). So for a fixed right end, the valid starts are not "everything from lo to hi"; they form a band in the middle, and one window cannot track both of its edges in a single pointer.
"At most k distinct" does survive shrinking, and the previous problems count it in one pass: shrink while
there are more than k values, then add hi − lo + 1. Every subarray with at most k values has
either exactly k or at most k − 1, so the subarrays with exactly k are counted by
atMost(k) − atMost(k − 1). Two linear passes, each with its own map.
The identity is worth remembering on its own: it also turns "exactly k odd numbers" and "sum exactly equal to S with non-negative numbers" into pairs of window passes.
Complexity. Time O(n) · Space O(k)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
from collections import defaultdict
def count_exactly_k(nums, k):
def at_most(k):
count = defaultdict(int)
total = lo = 0
for hi, x in enumerate(nums):
count[x] += 1
while len(count) > k: # too many values: shrink
count[nums[lo]] -= 1
if count[nums[lo]] == 0:
del count[nums[lo]]
lo += 1
total += hi - lo + 1 # every start from lo to hi has at most k
return total
return at_most(k) - at_most(k - 1)#include <vector>
static long long atMost(const std::vector<int>& nums, int k) {
std::vector<int> count(nums.size() + 1, 0); // values are 1..n
long long total = 0;
int kinds = 0, lo = 0;
for (int hi = 0; hi < (int)nums.size(); ++hi) {
if (count[nums[hi]]++ == 0) ++kinds;
while (kinds > k) if (--count[nums[lo++]] == 0) --kinds; // too many values: shrink
total += hi - lo + 1; // every start from lo to hi has at most k
}
return total;
}
long long solve(const std::vector<int>& nums, int k) {
return atMost(nums, k) - atMost(nums, k - 1);
}class Solution {
private long atMost(int[] nums, int k) {
int[] count = new int[nums.length + 1]; // values are 1..n
long total = 0;
int kinds = 0, lo = 0;
for (int hi = 0; hi < nums.length; hi++) {
if (count[nums[hi]]++ == 0) kinds++;
while (kinds > k) if (--count[nums[lo++]] == 0) kinds--; // too many values: shrink
total += hi - lo + 1;
}
return total;
}
public long countExactlyK(int[] nums, int k) {
return atMost(nums, k) - atMost(nums, k - 1);
}
}function countExactlyK(nums, k) {
const atMost = k => {
const count = new Map();
let total = 0, lo = 0;
for (let hi = 0; hi < nums.length; hi++) {
count.set(nums[hi], (count.get(nums[hi]) || 0) + 1);
while (count.size > k) { // too many values: shrink
const n = count.get(nums[lo]) - 1;
if (n === 0) count.delete(nums[lo]); else count.set(nums[lo], n);
lo++;
}
total += hi - lo + 1; // every start from lo to hi has at most k
}
return total;
};
return atMost(k) - atMost(k - 1);
}fn at_most(nums: &[i64], k: usize) -> i64 {
let mut count = vec![0usize; nums.len() + 1]; // values are 1..n
let (mut kinds, mut lo, mut total) = (0usize, 0usize, 0i64);
for hi in 0..nums.len() {
let x = nums[hi] as usize;
if count[x] == 0 { kinds += 1; }
count[x] += 1;
while kinds > k { // too many values: shrink
let y = nums[lo] as usize;
count[y] -= 1;
if count[y] == 0 { kinds -= 1; }
lo += 1;
}
total += (hi + 1 - lo) as i64;
}
total
}
fn solve(nums: &[i64], k: usize) -> i64 {
at_most(nums, k) - at_most(nums, k - 1)
}import qualified Data.Map.Strict as M
-- Exactly k = at most k − at most (k − 1); each pass is a window that counts its starts.
solve :: [Int] -> Int -> Int
solve nums k = atMost k - atMost (k - 1)
where
atMost kk = total
where
(_, _, _, total) = foldl step (M.empty, nums, 0, 0) nums
step (m, win, len, t) x =
let (m', win', len') = shrink (M.insertWith (+) x 1 m) win (len + 1)
in (m', win', len', t + len')
shrink m win len
| M.size m > kk, (y:ys) <- win =
shrink (M.update (\c -> if c == 1 then Nothing else Just (c - 1)) y m) ys (len - 1)
| otherwise = (m, win, len)
Variants to try
- Solve it in one pass with two left pointers, one for "at most k" and one for "at most k − 1"; it is the same identity, fused.
- Exactly k odd numbers: replace "distinct values" with "odd numbers" in atMost.
Next problems
Count runs whose product is below k
Counting windows rather than measuring them: once the window ending at hi is the longest valid one, all of its suffixes are valid too.
Open the problem → Medium20 minCount substrings holding a, b and c
When validity survives growing, count from the other side: after shrinking to the last valid start, every earlier start works too.
Open the problem → Easy10 minBest total over k consecutive days
A window of fixed width slides one place at a time: add the element that enters, subtract the one that leaves.
Open the problem →Write a solution and run it against the real test table.
Keep going
Count runs whose product is below k
Counting windows rather than measuring them: once the window ending at hi is the longest valid one, all of its suffixes are valid too.
Open the problem → Medium20 minCount substrings holding a, b and c
When validity survives growing, count from the other side: after shrinking to the last valid start, every earlier start works too.
Open the problem → Easy10 minBest total over k consecutive days
A window of fixed width slides one place at a time: add the element that enters, subtract the one that leaves.
Open the problem → Sliding windowThe pattern behind it
A window that grows on the right, shrinks on the left, and holds an invariant at all times.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.