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.
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 windowcur
: An integer tracking the total number of identical pairs in the current windowi
: The left pointer of the sliding windowans
: The cumulative count of good subarrays
Algorithm Steps:
-
Expand the window to the right: For each element
x
innums
, we treat it as the right boundary of our window:- Before adding
x
, we havecnt[x]
occurrences ofx
in the window - Adding one more
x
createscnt[x]
new pairs (pairing with each existingx
) - Update:
cur += cnt[x]
to add these new pairs - Update:
cnt[x] += 1
to increment the count ofx
- Before adding
-
Shrink from the left to find the optimal left boundary: We want to find the rightmost position
i
where removingnums[i]
would make the window have fewer thank
pairs:while cur - cnt[nums[i]] + 1 >= k:
This condition checks: if we remove
nums[i]
, would we still have at leastk
pairs?- Removing
nums[i]
would eliminatecnt[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)
- Removing
-
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 from0
toi
can serve as valid starting points for good subarrays ending at the current position, giving usi + 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 EvaluatorExample 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
, addi + 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
, addi + 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
, addi + 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]
- has 3 pairs[1, 1, 1, 2]
- has 3 pairs[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)
Which of the following is a good use case for backtracking?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!