992. Subarrays with K Different Integers - Explanation

Problem Link

Description

You are given an integer array nums and an integer k, return the number of good subarrays of nums.

A good array is an array where the number of different integers in that array is exactly k.

  • For example, [1,2,3,1,2] has 3 different integers: 1, 2, and 3.

A subarray is a contiguous part of an array.

Example 1:

Input: nums = [1,2,1,2,3], k = 2

Output: 7

Explanation: Subarrays formed with exactly 2 different integers: [1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2]

Example 2:

Input: nums = [1,2,1,3,4], k = 3

Output: 3

Explanation: Subarrays formed with exactly 3 different integers: [1,2,1,3], [2,1,3], [1,3,4].

Example 3:

Input: nums = [1,2,2], k = 1

Output: 4

Explanation: Subarrays formed with exactly 1 different integers:

Constraints:

  • 1 <= nums.length <= 20,000
  • 1 <= nums[i], k <= nums.length


Topics


Prerequisites

Before attempting this problem, you should be comfortable with:

  • Sliding Window - The optimal solutions use sliding window technique with variable window sizes to track subarrays
  • Hash Maps - Used to track frequency counts of elements within the current window
  • Two Pointers - Advanced solutions use multiple left pointers to track ranges of valid starting positions

1. Brute Force

Intuition

The most straightforward approach is to check every possible subarray and count those with exactly k distinct integers. For each starting index, we expand the subarray one element at a time, tracking distinct values using a set. Once we exceed k distinct values, we stop and move to the next starting position.

Algorithm

  1. Initialize a result counter res to store the count of valid subarrays.
  2. For each starting index i:
    • Create an empty set to track distinct values.
    • Expand the subarray by moving index j from i to the end.
    • Add each new element to the set.
    • If the set size exceeds k, break out of the inner loop.
    • If the set size equals k, increment res.
  3. Return res.
class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
        n, res = len(nums), 0

        for i in range(n):
            seen = set()
            for j in range(i, n):
                seen.add(nums[j])
                if len(seen) > k:
                    break

                if len(seen) == k:
                    res += 1

        return res

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(n)O(n)

2. Sliding Window

Intuition

Counting subarrays with exactly k distinct values is tricky because adding an element can either increase or maintain the count of distinct values. A clever observation is that we can reframe the problem: subarrays with exactly k distinct = subarrays with at most k distinct minus subarrays with at most k-1 distinct. Counting "at most k" is easier using a sliding window that shrinks whenever we exceed k distinct values.

Algorithm

  1. Define a helper function atMostK(k) that counts subarrays with at most k distinct values:
    • Use two pointers l and r to define the window.
    • Maintain a frequency map to track counts of elements in the window.
    • Expand r and add each element to the map.
    • When distinct count exceeds k, shrink from l until we have at most k distinct.
    • For each position of r, add (r - l + 1) to the result, representing all valid subarrays ending at r.
  2. Return atMostK(k) - atMostK(k - 1).
class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:

        def atMostK(k):
            count = defaultdict(int)
            res = l = 0

            for r in range(len(nums)):
                count[nums[r]] += 1
                if count[nums[r]] == 1:
                    k -= 1

                while k < 0:
                    count[nums[l]] -= 1
                    if count[nums[l]] == 0:
                        k += 1
                    l += 1

                res += (r - l + 1)

            return res

        return atMostK(k) - atMostK(k - 1)

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

3. Sliding Window (One Pass) - I

Intuition

Instead of computing "at most k" twice, we can count exactly k in a single pass by tracking a range of valid left boundaries. For any right pointer position with exactly k distinct values, there may be multiple valid starting positions. We maintain two left pointers: l_far marks the leftmost valid start, and l_near marks the rightmost valid start (where the leftmost element appears exactly once). The count of valid subarrays ending at r is l_near - l_far + 1.

Algorithm

  1. Initialize l_far and l_near to 0, and a frequency map count.
  2. For each right pointer r:
    • Add nums[r] to the frequency map.
    • If distinct count exceeds k, shrink by moving l_near right and removing elements until we have k distinct. Set l_far = l_near.
    • While the leftmost element has count greater than 1, decrement its count and move l_near right.
    • If we have exactly k distinct values, add l_near - l_far + 1 to the result.
  3. Return the result.
class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
        count = defaultdict(int)
        res = 0
        l_far = 0
        l_near = 0

        for r in range(len(nums)):
            count[nums[r]] += 1

            while len(count) > k:
                count[nums[l_near]] -= 1
                if count[nums[l_near]] == 0:
                    count.pop(nums[l_near])
                l_near += 1
                l_far = l_near

            while count[nums[l_near]] > 1:
                count[nums[l_near]] -= 1
                l_near += 1

            if len(count) == k:
                res += l_near - l_far + 1

        return res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

4. Sliding Window (One Pass) - II

Intuition

This approach simplifies the previous one by using a single left pointer and a counter cnt to track how many positions we can extend the left boundary. When we have exactly k distinct values, we shrink the window from the left as long as the leftmost element has duplicates, incrementing cnt each time. The number of valid subarrays ending at the current position is cnt + 1.

Algorithm

  1. Create an array count of size n + 1 for frequency tracking (since values are in range [1, n]).
  2. Initialize l, cnt, and res to 0.
  3. For each right pointer r:
    • Increment count[nums[r]]. If this is a new distinct value, decrement k.
    • If k < 0 (more than k distinct), decrement count[nums[l]], increment l, increment k, and reset cnt = 0.
    • If k == 0 (exactly k distinct), shrink from the left while count[nums[l]] > 1, incrementing cnt each time.
    • Add cnt + 1 to res.
  4. Return res.
class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
        n = len(nums)
        count = [0] * (n + 1)
        res = l = cnt = 0

        for r in range(n):
            count[nums[r]] += 1
            if count[nums[r]] == 1:
                k -= 1

            if k < 0:
                count[nums[l]] -= 1
                l += 1
                k += 1
                cnt = 0

            if k == 0:
                while count[nums[l]] > 1:
                    count[nums[l]] -= 1
                    l += 1
                    cnt += 1

                res += (cnt + 1)

        return res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

Common Pitfalls

Trying to Count Exactly K Directly with a Single Sliding Window

A single sliding window can only count subarrays with "at most k" distinct values, not "exactly k." To count exactly k, you must use the formula atMostK(k) - atMostK(k-1) or maintain two pointers to track a range of valid left boundaries.

Off-by-One Errors in Subarray Counting

When counting subarrays ending at position r, the count is r - l + 1, not r - l. This represents all subarrays starting at any position from l to r and ending at r. Forgetting the +1 leads to undercounting.

Not Properly Tracking When Distinct Count Changes

When a new element is added, increment the distinct count only if its frequency becomes 1 (first occurrence). When removing, decrement the distinct count only when its frequency drops to 0. Using set size directly on every operation is less efficient and error-prone.

Resetting State Incorrectly in the One-Pass Approach

In the one-pass sliding window variant, when the distinct count exceeds k, you must reset the accumulated count cnt to 0. Forgetting this reset causes the algorithm to incorrectly carry over counts from invalid window states.

Confusing the Two Left Pointers in One-Pass Solutions

The one-pass approach uses two left pointers: l_far (leftmost valid start) and l_near (rightmost valid start where the leftmost element appears exactly once). Mixing up their roles or forgetting to update l_far = l_near when shrinking past k distinct values produces incorrect results.