A Harder Count

You can find the longest subarray satisfying a condition. You can find the shortest. You can track frequency maps, maintain valid windows, and slide efficiently.

But what about counting? Not finding one best window --- counting every subarray with exactly K distinct integers.

Surely the expand/shrink protocol handles this too?

Try it mentally. Take the array [1, 2, 1, 2, 3] with K = 2. Start with the window [1, 2] --- that has exactly 2 distinct integers, so it counts. Expand to [1, 2, 1] --- still 2 distinct, still counts. Expand to [1, 2, 1, 2] --- still 2 distinct, still counts. Now expand to [1, 2, 1, 2, 3] --- 3 distinct integers. Too many. Time to shrink.

Shrink to [2, 1, 2, 3] --- still 3 distinct. Shrink to [1, 2, 3] --- still 3 distinct. Shrink to [2, 3] --- exactly 2 distinct. Good, that counts. But wait --- did you count [2, 1, 2]? That also has exactly 2 distinct integers, and it was a valid window before you shrank past it. The standard expand/shrink loop blew right past it.

The problem is that “exactly K” is not a monotonic constraint. Adding an element can push you over K, and removing one can push you under K. There is no clean shrink direction --- you need to go both ways. A single sliding window cannot handle this.

Or can it?

Try It Yourself

Let's see if your sliding window instincts can handle this. The array is [1, 2, 1, 2, 3] and you need to count every subarray with exactly 2 distinct integers. Not the longest one. Not the shortest one. The total count.

Before you begin, try to enumerate them in your head. [1, 2] has 2 distinct --- that counts. [2, 1] also has 2 distinct. [1, 2, 1] has 2 distinct. [2, 1, 2] has 2 distinct. There are more. How many total? And can you find them all with expand/shrink?

Here is where it gets uncomfortable. If your window has exactly 2 distinct integers and you expand, you might jump to 3 distinct (too many). If you shrink, you might drop to 1 distinct (too few). The “exactly” constraint squeezes you from both sides. There is no safe direction to move. With “at most K” you had a clear shrink target --- keep going until distinct count drops below the threshold. With “at least K” you had a clear expand target. But “exactly K”? Both directions are dangerous.

Try it anyway. Use the tools you have and notice the exact moment the standard approach fails. Which valid subarrays slip through the cracks? That gap is what the next screen will address.

The Exactly-K Trap

Count subarrays with exactly 2 distinct integers in 12123. You have a sliding window. Try it.

1
2
1
2
3
freq:
(0 distinct)

Counting Subarrays Inside the Window

There is a subtle piece of machinery inside atMost(K) that makes the whole counting approach work. At each position of right, you don't just have one valid subarray --- you have several. But how many?

Picture it. The window spans indices [left .. right], and every element in that range satisfies the constraint. That means any subarray that ends at right and starts at any index from left to right is valid. Starting at left gives the full window. Starting at left + 1 gives a slightly shorter one. All the way down to starting at right itself --- a single-element subarray.

At right = 2, the window spans indices 012. How many valid subarrays END at index 2?

That is the formula: right - left + 1. You don't enumerate the subarrays. You add that number to a running total. Every valid subarray gets counted exactly once --- at the moment right reaches its rightmost element.

Here is a concrete trace. For [1, 2, 1, 2, 3] with atMost(2):

  • right = 0: window [1], 1 distinct. Count += 1. (Subarrays: [1])
  • right = 1: window [1, 2], 2 distinct. Count += 2. (Subarrays: [1, 2], [2])
  • right = 2: window [1, 2, 1], 2 distinct. Count += 3. (Subarrays: [1, 2, 1], [2, 1], [1])
  • right = 3: window [1, 2, 1, 2], 2 distinct. Count += 4.
  • right = 4: element 3 enters, 3 distinct. Shrink: remove arr[0]=1 (still 3 distinct), remove arr[1]=2 (still 3), remove arr[2]=1 (now 2 distinct). Left = 3. Count += 2. (Subarrays: [2, 3], [3])

Total for atMost(2): 1 + 2 + 3 + 4 + 2 = 12.

Notice something about step 4 --- right = 4. The window had to shrink past indices 0, 1, and 2 because the 3 introduced a third distinct value. But it stopped at left = 3, not left = 4 --- the window [2, 3] has only 2 distinct values, so the shrink loop exits. The formula still works: 4 - 3 + 1 = 2.

This counting trick appears in every “count subarrays satisfying X” problem. The variable window gives you the valid range; the formula right - left + 1 gives you the count at each step.

The Subtraction Identity

You just felt the squeeze --- “exactly K” is not monotonic, and a single sliding window cannot handle it directly. But there is a decomposition that transforms the problem into two calls to a function you already know how to write. Before it is revealed:

To count windows with EXACTLY k distinct characters, which formula works?

Here is the identity:

1
exactly(K) = atMost(K) - atMost(K - 1)

Think about what atMost(K) counts: every subarray with 1, 2, 3, ..., or K distinct integers. And atMost(K - 1) counts every subarray with 1, 2, 3, ..., or K - 1 distinct integers. Subtracting removes everything with fewer than K distinct integers, leaving only the subarrays with exactly K.

Make sure that clicks before you see the numbers:

What does atMost(2) - atMost(1) actually compute?

For our example with K = 2:

  • atMost(2) = 12 (subarrays with 1 or 2 distinct integers)
  • atMost(1) = 5 (subarrays with exactly 1 distinct integer: [1], [2], [1], [2], [3])
  • exactly(2) = 12 - 5 = 7

Here is the complete solution for LC 992 (Subarrays with K Different Integers):

1
function subarraysWithKDistinct(nums: number[], k: number): number {
2
  return atMost(nums, k) - atMost(nums, k - 1);
3
}
4
5
function atMost(nums: number[], k: number): number {
6
  const freq = new Map<number, number>();
7
  let left = 0, count = 0;
8
9
  for (let right = 0; right < nums.length; right++) {
10
    freq.set(nums[right], (freq.get(nums[right]) ?? 0) + 1);
11
12
    while (freq.size > k) {
13
      const leftVal = nums[left];
14
      freq.set(leftVal, freq.get(leftVal)! - 1);
15
      if (freq.get(leftVal) === 0) freq.delete(leftVal);
16
      left++;
17
    }
18
    count += right - left + 1;
19
  }
20
  return count;
21
}

Notice how atMost is just the standard variable-size window with while-shrink. The only twist is the counting formula right - left + 1 at each step. The subtraction identity transforms an impossible direct constraint into two calls to a function you already know how to write.

This is a general technique. Anytime a constraint is not monotonic but can be expressed as the difference of two monotonic constraints, you can decompose it. “Exactly K” is the most common instance, but the same trick works for “exactly K zeros in a binary array” or “exactly K vowels in a substring.”