Facebook Pixel

2537. Count the Number of Good Subarrays

Problem Description

You are given an integer array nums and an integer k. Your task is to find the number of good subarrays in nums.

A subarray is considered good if it contains at least k pairs of indices (i, j) where:

  • i < j
  • arr[i] == arr[j] (the elements at these indices are equal)

A subarray is a contiguous non-empty sequence of elements within the array.

For example, if we have a subarray [1, 2, 1, 2, 1]:

  • The pairs with equal elements are: indices (0, 2), (0, 4), (2, 4) for element 1, and indices (1, 3) for element 2
  • This gives us a total of 4 pairs

The problem asks you to count how many subarrays of nums have at least k such pairs of equal elements.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key observation is that if a subarray already has at least k pairs of identical elements, then extending this subarray to the right will only add more pairs (or keep the same number), never decrease them. This means once we find a good subarray, all its extensions to the right are also good.

When we add a new element to a subarray, how many new pairs does it create? If the element x already appears cnt[x] times in the subarray, adding one more x creates exactly cnt[x] new pairs (pairing with each existing occurrence of x).

This suggests using a sliding window approach with two pointers. We can expand the window to the right by adding elements one by one. For each new element, we calculate how many new pairs it creates with existing elements in the window.

The clever part is determining the leftmost valid position. Once we have at least k pairs in our window, we want to find the rightmost left boundary where the window still has at least k pairs. We can do this by shrinking from the left: keep removing the leftmost element as long as the remaining window still has at least k pairs.

Why does this work? Because if removing the leftmost element still leaves us with at least k pairs, then that element (and all elements to its left) can serve as valid starting points for subarrays ending at the current position. This is because adding back those elements would only increase the number of pairs.

For counting, once we've found the rightmost valid left boundary at position i, all positions from 0 to i can be valid starting points for good subarrays ending at the current position. So we add i + 1 to our answer.

Learn more about Sliding Window patterns.

Solution Approach

We implement a sliding window approach using two pointers with a hash table to track element frequencies.

Data Structures:

  • cnt: A Counter (hash table) to track the frequency of each element in the current window
  • cur: An integer tracking the total number of identical pairs in the current window
  • i: The left pointer of the sliding window
  • ans: The cumulative count of good subarrays

Algorithm Steps:

  1. Expand the window to the right: For each element x in nums, we treat it as the right boundary of our window:

    • Before adding x, we have cnt[x] occurrences of x in the window
    • Adding one more x creates cnt[x] new pairs (pairing with each existing x)
    • Update: cur += cnt[x] to add these new pairs
    • Update: cnt[x] += 1 to increment the count of x
  2. Shrink from the left to find the optimal left boundary: We want to find the rightmost position i where removing nums[i] would make the window have fewer than k pairs:

    while cur - cnt[nums[i]] + 1 >= k:

    This condition checks: if we remove nums[i], would we still have at least k pairs?

    • Removing nums[i] would eliminate cnt[nums[i]] - 1 pairs (pairs between this element and other occurrences of the same value)
    • If yes, we can safely remove it:
      • cnt[nums[i]] -= 1 (decrement the count)
      • cur -= cnt[nums[i]] (remove the pairs - note we use the updated count)
      • i += 1 (move the left boundary right)
  3. Count valid subarrays: After finding the optimal left boundary:

    if cur >= k:
        ans += i + 1

    If the current window has at least k pairs, then all positions from 0 to i can serve as valid starting points for good subarrays ending at the current position, giving us i + 1 valid subarrays.

The time complexity is O(n) since each element is added and removed at most once. The space complexity is O(n) in the worst case for the hash table.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with nums = [1, 1, 1, 2, 2] and k = 3.

We'll track the window [i, j], the counter cnt, current pairs cur, and running answer ans.

Initial state: i = 0, cnt = {}, cur = 0, ans = 0

Step 1: j = 0, element = 1

  • Before adding: cnt[1] = 0, so adding creates 0 new pairs
  • Update: cur = 0, cnt = {1: 1}
  • Check shrinking: cur - cnt[nums[0]] + 1 = 0 - 1 + 1 = 0 < 3, so don't shrink
  • Since cur < k, don't add to answer
  • Window: [1], pairs: 0

Step 2: j = 1, element = 1

  • Before adding: cnt[1] = 1, so adding creates 1 new pair (indices 0,1)
  • Update: cur = 1, cnt = {1: 2}
  • Check shrinking: cur - cnt[nums[0]] + 1 = 1 - 2 + 1 = 0 < 3, so don't shrink
  • Since cur < k, don't add to answer
  • Window: [1, 1], pairs: 1

Step 3: j = 2, element = 1

  • Before adding: cnt[1] = 2, so adding creates 2 new pairs (indices 0,2 and 1,2)
  • Update: cur = 3, cnt = {1: 3}
  • Check shrinking: cur - cnt[nums[0]] + 1 = 3 - 3 + 1 = 1 < 3, so don't shrink
  • Since cur >= k, add i + 1 = 1 to answer. ans = 1
  • Window: [1, 1, 1], pairs: 3
  • This means subarray [1,1,1] starting at index 0 is good

Step 4: j = 3, element = 2

  • Before adding: cnt[2] = 0, so adding creates 0 new pairs
  • Update: cur = 3, cnt = {1: 3, 2: 1}
  • Check shrinking: cur - cnt[nums[0]] + 1 = 3 - 3 + 1 = 1 < 3, so don't shrink
  • Since cur >= k, add i + 1 = 1 to answer. ans = 2
  • Window: [1, 1, 1, 2], pairs: 3
  • This means subarray [1,1,1,2] starting at index 0 is good

Step 5: j = 4, element = 2

  • Before adding: cnt[2] = 1, so adding creates 1 new pair (indices 3,4)
  • Update: cur = 4, cnt = {1: 3, 2: 2}
  • Check shrinking:
    • cur - cnt[nums[0]] + 1 = 4 - 3 + 1 = 2 < 3? No, it equals 2 which is less than 3
    • So we don't shrink
  • Since cur >= k, add i + 1 = 1 to answer. ans = 3
  • Window: [1, 1, 1, 2, 2], pairs: 4
  • This means subarray [1,1,1,2,2] starting at index 0 is good

Final answer: 3

The good subarrays are:

  1. [1, 1, 1] - has 3 pairs
  2. [1, 1, 1, 2] - has 3 pairs
  3. [1, 1, 1, 2, 2] - has 4 pairs

Solution Implementation

1class Solution:
2    def countGood(self, nums: List[int], k: int) -> int:
3        # Dictionary to track frequency of each number in current window
4        frequency_map = Counter()
5      
6        # total_subarrays: count of valid subarrays
7        # pair_count: current number of equal pairs in the window
8        total_subarrays = 0
9        pair_count = 0
10      
11        # Left pointer of the sliding window
12        left = 0
13      
14        # Iterate through array with right pointer
15        for num in nums:
16            # Adding num to window creates frequency_map[num] new pairs
17            # (pairs with all existing occurrences of num)
18            pair_count += frequency_map[num]
19            frequency_map[num] += 1
20          
21            # Shrink window from left while maintaining at least k pairs
22            # Check if removing nums[left] still keeps >= k pairs
23            while pair_count - frequency_map[nums[left]] + 1 >= k:
24                frequency_map[nums[left]] -= 1
25                # After decrement, nums[left] forms frequency_map[nums[left]] pairs
26                pair_count -= frequency_map[nums[left]]
27                left += 1
28          
29            # If current window has >= k pairs, all subarrays ending at current
30            # position with start index from 0 to left are valid
31            if pair_count >= k:
32                total_subarrays += left + 1
33              
34        return total_subarrays
35
1class Solution {
2    public long countGood(int[] nums, int k) {
3        // Map to store frequency of each number in current window
4        Map<Integer, Integer> frequencyMap = new HashMap<>();
5      
6        // Total count of good subarrays
7        long totalGoodSubarrays = 0;
8      
9        // Current count of pairs in the window
10        long currentPairs = 0;
11      
12        // Left pointer of the sliding window
13        int leftPointer = 0;
14      
15        // Iterate through array with right pointer
16        for (int currentNum : nums) {
17            // Add current number to window and update pair count
18            // When adding a number that appears n times, it forms (n-1) new pairs
19            int newFrequency = frequencyMap.merge(currentNum, 1, Integer::sum);
20            currentPairs += newFrequency - 1;
21          
22            // Shrink window from left while maintaining at least k pairs
23            // Check if removing the leftmost element still keeps pairs >= k
24            while (currentPairs - frequencyMap.get(nums[leftPointer]) + 1 >= k) {
25                // Remove leftmost element and update pair count
26                // When removing a number that appears n times, we lose (n-1) pairs
27                int updatedFrequency = frequencyMap.merge(nums[leftPointer], -1, Integer::sum);
28                currentPairs -= updatedFrequency;
29                leftPointer++;
30            }
31          
32            // If current window has at least k pairs, all subarrays starting
33            // from indices [0, leftPointer] and ending at current position are good
34            if (currentPairs >= k) {
35                totalGoodSubarrays += leftPointer + 1;
36            }
37        }
38      
39        return totalGoodSubarrays;
40    }
41}
42
1class Solution {
2public:
3    long long countGood(vector<int>& nums, int k) {
4        // Hash map to store frequency of each number in current window
5        unordered_map<int, int> frequency;
6      
7        // Total count of good subarrays
8        long long result = 0;
9      
10        // Current count of pairs in the window
11        long long pairCount = 0;
12      
13        // Left pointer of the sliding window
14        int left = 0;
15      
16        // Iterate through array with right pointer
17        for (int& num : nums) {
18            // Add pairs formed by adding current number
19            // If frequency[num] = f, adding one more creates f new pairs
20            pairCount += frequency[num]++;
21          
22            // Shrink window from left while maintaining at least k pairs
23            // Check if removing nums[left] still keeps us at >= k pairs
24            while (pairCount - frequency[nums[left]] + 1 >= k) {
25                // Remove the leftmost element and update pair count
26                // Decrement frequency first, then use the new value
27                pairCount -= --frequency[nums[left++]];
28            }
29          
30            // If current window has at least k pairs
31            // All subarrays ending at current position with start from [0, left] are valid
32            if (pairCount >= k) {
33                result += left + 1;
34            }
35        }
36      
37        return result;
38    }
39};
40
1/**
2 * Counts the number of good subarrays where a subarray is good if it has at least k pairs of equal elements
3 * @param nums - The input array of numbers
4 * @param k - The minimum number of pairs required for a subarray to be good
5 * @returns The total count of good subarrays
6 */
7function countGood(nums: number[], k: number): number {
8    // Map to store frequency of each number in current window
9    const frequencyMap: Map<number, number> = new Map();
10  
11    // totalGoodSubarrays: accumulates the answer
12    // currentPairCount: number of pairs in current window
13    // leftPointer: left boundary of sliding window
14    let totalGoodSubarrays: number = 0;
15    let currentPairCount: number = 0;
16    let leftPointer: number = 0;
17
18    // Iterate through array with right pointer
19    for (const currentNum of nums) {
20        // Get current frequency of the number (default to 0 if not exists)
21        const currentFrequency: number = frequencyMap.get(currentNum) || 0;
22      
23        // Adding this number creates 'currentFrequency' new pairs with existing occurrences
24        currentPairCount += currentFrequency;
25      
26        // Update frequency map with incremented count
27        frequencyMap.set(currentNum, currentFrequency + 1);
28
29        // Shrink window from left while maintaining at least k pairs
30        // Check if removing left element still keeps us with >= k pairs
31        while (currentPairCount - (frequencyMap.get(nums[leftPointer])! - 1) >= k) {
32            const leftElementFrequency: number = frequencyMap.get(nums[leftPointer])!;
33          
34            // Update frequency map by decrementing count
35            frequencyMap.set(nums[leftPointer], leftElementFrequency - 1);
36          
37            // Removing this element reduces pairs by (frequency - 1)
38            currentPairCount -= leftElementFrequency - 1;
39          
40            // Move left pointer forward
41            leftPointer += 1;
42        }
43
44        // If current window has at least k pairs, all subarrays ending at current position
45        // and starting from index 0 to leftPointer are valid
46        if (currentPairCount >= k) {
47            totalGoodSubarrays += leftPointer + 1;
48        }
49    }
50
51    return totalGoodSubarrays;
52}
53

Time and Space Complexity

Time Complexity: O(n) where n is the length of the array nums.

The algorithm uses a two-pointer approach with a sliding window technique. The outer loop iterates through each element in nums exactly once, contributing O(n) operations. The inner while loop might seem to add complexity, but each element can only be removed from the window once. Specifically, the variable i only moves forward and can increment at most n times throughout the entire execution. Therefore, the total number of operations across all iterations is bounded by O(n) for the outer loop and O(n) for all executions of the inner loop combined, resulting in an overall time complexity of O(n).

Space Complexity: O(n) where n is the length of the array nums.

The space complexity is determined by the Counter object cnt, which stores the frequency of elements in the current window. In the worst case, when all elements in nums are distinct, the counter would need to store up to n unique keys and their corresponding counts, requiring O(n) space. The remaining variables (ans, cur, i, x) use constant space O(1), so the overall space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrect Order of Operations When Updating Pairs

The Problem: A critical mistake occurs when updating the pair_count after modifying the frequency map. The order matters significantly:

# WRONG - This will use the updated frequency incorrectly
frequency_map[num] += 1
pair_count += frequency_map[num]  # This adds one too many pairs!

When a new element is added to the window, it forms pairs with all existing occurrences of the same value. If we increment the frequency first, we're incorrectly counting a pair between the element and itself.

The Solution: Always update pair_count before updating frequency_map:

# CORRECT
pair_count += frequency_map[num]  # Add pairs with existing occurrences
frequency_map[num] += 1            # Then increment the frequency

Similarly, when removing an element:

# CORRECT
frequency_map[nums[left]] -= 1              # Decrement first
pair_count -= frequency_map[nums[left]]     # Then subtract remaining pairs

Pitfall 2: Off-by-One Error in the While Loop Condition

The Problem: The condition for shrinking the window can be confusing:

# This might seem intuitive but is WRONG
while pair_count - (frequency_map[nums[left]] - 1) >= k:

The confusion arises from mixing up "pairs that would be removed" with "pairs that would remain."

The Solution: Think of it as: "After removing nums[left], how many pairs would that element still contribute?"

  • Before removal: frequency_map[nums[left]] occurrences
  • After removal: frequency_map[nums[left]] - 1 occurrences
  • Pairs after removal: frequency_map[nums[left]] - 1 pairs
# CORRECT - Check if we'd still have k pairs after removal
while pair_count - (frequency_map[nums[left]] - 1) >= k:

Pitfall 3: Not Handling Empty Frequency Map Entries

The Problem: Using a regular dictionary instead of Counter can lead to KeyError:

# WRONG - Will raise KeyError for new elements
frequency_map = {}
pair_count += frequency_map[num]  # KeyError if num not in map!

The Solution: Use Counter which returns 0 for missing keys, or explicitly handle missing keys:

# Option 1: Use Counter (recommended)
frequency_map = Counter()

# Option 2: Use defaultdict
frequency_map = defaultdict(int)

# Option 3: Explicit checking (not recommended, more verbose)
frequency_map = {}
pair_count += frequency_map.get(num, 0)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More