3583. Count Special Triplets
Problem Description
You are given an integer array nums
.
The problem asks you to find all special triplets in the array. A special triplet consists of three indices (i, j, k)
that satisfy the following conditions:
- The indices must be in strictly increasing order:
0 <= i < j < k < n
, wheren
is the length of the array - The element at index
i
must equal twice the element at indexj
:nums[i] == nums[j] * 2
- The element at index
k
must also equal twice the element at indexj
:nums[k] == nums[j] * 2
In other words, you need to find triplets where the first and third elements are both exactly double the middle element, and they appear in that specific order in the array.
For example, if you have an array like [4, 2, 4]
, this forms a special triplet with indices (0, 1, 2)
because:
nums[0] = 4 = 2 * 2 = nums[1] * 2
nums[2] = 4 = 2 * 2 = nums[1] * 2
Your task is to count the total number of such special triplets in the given array. Since the answer can be very large, you should return the result modulo 10^9 + 7
.
Intuition
The key insight is to think about this problem from the perspective of the middle element rather than trying to find all valid triplets directly.
If we fix the middle element nums[j]
, then we need to find:
- How many elements equal to
nums[j] * 2
appear before indexj
(these can be ournums[i]
) - How many elements equal to
nums[j] * 2
appear after indexj
(these can be ournums[k]
)
The total number of special triplets with nums[j]
as the middle element would be the product of these two counts, since we can pair any valid i
with any valid k
.
To efficiently track these counts, we can use two hash tables:
- A
left
counter to track elements we've already seen (to the left of current position) - A
right
counter to track elements we haven't processed yet (to the right of current position)
As we traverse the array from left to right, for each element that could serve as the middle element:
- We remove it from the
right
counter (since it's no longer to the right of our current position) - We check how many times
nums[j] * 2
appears in bothleft
andright
counters - We multiply these counts to get the number of valid triplets with this middle element
- We add the current element to the
left
counter for future iterations
This approach cleverly transforms a seemingly complex three-pointer problem into a single-pass solution with hash tables, reducing the time complexity while maintaining correctness.
Solution Approach
The solution implements the enumerate-middle-element strategy using hash tables to efficiently count valid triplets.
Data Structures Used:
left
: A Counter (hash table) that tracks the frequency of elements to the left of the current positionright
: A Counter that initially contains all elements and tracks frequencies to the right of the current position
Algorithm Steps:
-
Initialize the counters:
- Create an empty
left
counter - Create a
right
counter initialized with all elements fromnums
- Set
ans = 0
to accumulate the result - Define
mod = 10^9 + 7
for the modulo operation
- Create an empty
-
Iterate through each element as the middle element: For each element
x
innums
:a. Update right counter: Remove the current element from
right
by decrementing its count:right[x] -= 1
- This ensures
x
is not counted as being to the right of itself
b. Calculate triplets with
x
as middle:- The number we're looking for on both sides is
x * 2
- Count of valid
i
indices:left[x * 2]
(elements equal tox * 2
before current position) - Count of valid
k
indices:right[x * 2]
(elements equal tox * 2
after current position) - Add their product to the answer:
ans = (ans + left[x * 2] * right[x * 2]) % mod
c. Update left counter: Add the current element to
left
by incrementing its count:left[x] += 1
- This makes
x
available for future iterations as a left element
- This ensures
-
Return the final answer: After processing all elements, return
ans
Time Complexity: O(n)
where n
is the length of the array, as we make a single pass through the array with constant-time hash table operations.
Space Complexity: O(n)
for storing the two hash tables.
The beauty of this approach is that it avoids the naive O(n^3)
solution of checking all possible triplets by fixing the middle element and using multiplication principle to count valid combinations.
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 solution with the array nums = [1, 2, 4, 1, 2]
.
We're looking for triplets where nums[i] = nums[j] * 2
and nums[k] = nums[j] * 2
.
Initial Setup:
left = {}
(empty counter)right = {1: 2, 2: 2, 4: 1}
(frequency of all elements)ans = 0
Step 1: Process index 0, x = 1
- Remove from right:
right = {1: 1, 2: 2, 4: 1}
- Looking for
1 * 2 = 2
on both sides:left[2] = 0
(no 2's to the left)right[2] = 2
(two 2's to the right)- Add to answer:
ans += 0 * 2 = 0
- Add to left:
left = {1: 1}
Step 2: Process index 1, x = 2
- Remove from right:
right = {1: 1, 2: 1, 4: 1}
- Looking for
2 * 2 = 4
on both sides:left[4] = 0
(no 4's to the left)right[4] = 1
(one 4 to the right)- Add to answer:
ans += 0 * 1 = 0
- Add to left:
left = {1: 1, 2: 1}
Step 3: Process index 2, x = 4
- Remove from right:
right = {1: 1, 2: 1, 4: 0}
- Looking for
4 * 2 = 8
on both sides:left[8] = 0
(no 8's to the left)right[8] = 0
(no 8's to the right)- Add to answer:
ans += 0 * 0 = 0
- Add to left:
left = {1: 1, 2: 1, 4: 1}
Step 4: Process index 3, x = 1
- Remove from right:
right = {1: 0, 2: 1, 4: 0}
- Looking for
1 * 2 = 2
on both sides:left[2] = 1
(one 2 to the left at index 1)right[2] = 1
(one 2 to the right at index 4)- Add to answer:
ans += 1 * 1 = 1
- Add to left:
left = {1: 2, 2: 1, 4: 1}
Step 5: Process index 4, x = 2
- Remove from right:
right = {1: 0, 2: 0, 4: 0}
- Looking for
2 * 2 = 4
on both sides:left[4] = 1
(one 4 to the left at index 2)right[4] = 0
(no 4's to the right)- Add to answer:
ans += 1 * 0 = 0
- Add to left:
left = {1: 2, 2: 2, 4: 1}
Final Result: ans = 1
The special triplet found is (1, 3, 4)
with values (2, 1, 2)
where both nums[1] = 2
and nums[4] = 2
equal 2 * nums[3] = 2 * 1
.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def specialTriplets(self, nums: List[int]) -> int:
6 # Initialize counters for elements to the left and right of current position
7 left_counter = Counter() # Tracks frequency of elements seen so far (left side)
8 right_counter = Counter(nums) # Initially contains all elements (right side)
9
10 # Initialize result and modulo constant
11 result = 0
12 MOD = 10**9 + 7
13
14 # Iterate through each element as the middle element of a potential triplet
15 for current_num in nums:
16 # Remove current element from right counter (moving it from right to current position)
17 right_counter[current_num] -= 1
18
19 # Count triplets where current_num is the middle element
20 # Looking for pattern: left_element * 2 = current_num and current_num * 2 = right_element
21 # This means: left_element = current_num * 2 and right_element = current_num * 2
22 # So we're counting triplets of form (x*2, x, x*2)
23 double_value = current_num * 2
24 triplet_count = (left_counter[double_value] * right_counter[double_value]) % MOD
25 result = (result + triplet_count) % MOD
26
27 # Add current element to left counter (moving it from current position to left side)
28 left_counter[current_num] += 1
29
30 return result
31
1class Solution {
2 public int specialTriplets(int[] nums) {
3 // Map to count occurrences of elements to the left of current position
4 Map<Integer, Integer> leftCounts = new HashMap<>();
5
6 // Map to count occurrences of elements to the right of current position
7 // Initially contains all elements
8 Map<Integer, Integer> rightCounts = new HashMap<>();
9
10 // Initialize rightCounts with all elements from the array
11 for (int num : nums) {
12 rightCounts.merge(num, 1, Integer::sum);
13 }
14
15 // Variable to store the total count of special triplets
16 long totalCount = 0;
17
18 // Modulo value for preventing integer overflow
19 final int MOD = (int) 1e9 + 7;
20
21 // Iterate through each element as the potential middle element of a triplet
22 for (int currentNum : nums) {
23 // Remove current element from right counts (moving it from right to current position)
24 rightCounts.merge(currentNum, -1, Integer::sum);
25
26 // Calculate the target value that should appear on both sides
27 // For this to be a special triplet, we need elements equal to 2 * currentNum
28 int targetValue = currentNum * 2;
29
30 // Count triplets where left and right elements both equal targetValue
31 // and current element is in the middle
32 long leftCount = leftCounts.getOrDefault(targetValue, 0);
33 long rightCount = rightCounts.getOrDefault(targetValue, 0);
34 long tripletCount = (leftCount * rightCount) % MOD;
35
36 // Add the count of triplets with current element as middle
37 totalCount = (totalCount + tripletCount) % MOD;
38
39 // Add current element to left counts (moving it from current position to left)
40 leftCounts.merge(currentNum, 1, Integer::sum);
41 }
42
43 // Return the final count as an integer
44 return (int) totalCount;
45 }
46}
47
1class Solution {
2public:
3 int specialTriplets(vector<int>& nums) {
4 // Map to count occurrences of elements to the left of current position
5 unordered_map<int, int> leftCount;
6 // Map to count occurrences of elements to the right of current position
7 unordered_map<int, int> rightCount;
8
9 // Initialize rightCount with all elements
10 for (int num : nums) {
11 rightCount[num]++;
12 }
13
14 long long result = 0;
15 const int MOD = 1e9 + 7;
16
17 // Iterate through each element as the middle element of triplet
18 for (int middleValue : nums) {
19 // Remove current element from right side
20 rightCount[middleValue]--;
21
22 // Count triplets where current element is in the middle
23 // and left element equals right element equals 2 * middle element
24 // This forms a triplet (2x, x, 2x) where x is the middle value
25 int targetValue = middleValue * 2;
26 result = (result + 1LL * leftCount[targetValue] * rightCount[targetValue] % MOD) % MOD;
27
28 // Add current element to left side for next iterations
29 leftCount[middleValue]++;
30 }
31
32 return static_cast<int>(result);
33 }
34};
35
1/**
2 * Counts special triplets where the middle element times 2 equals both the left and right elements
3 * @param nums - Array of numbers to process
4 * @returns Number of special triplets modulo 10^9 + 7
5 */
6function specialTriplets(nums: number[]): number {
7 // Map to track frequency of elements to the left of current position
8 const leftFrequency = new Map<number, number>();
9
10 // Map to track frequency of elements to the right of current position
11 const rightFrequency = new Map<number, number>();
12
13 // Initialize right frequency map with all elements
14 for (const num of nums) {
15 rightFrequency.set(num, (rightFrequency.get(num) || 0) + 1);
16 }
17
18 // Counter for special triplets
19 let tripletCount = 0;
20
21 // Modulo value for preventing integer overflow
22 const MOD = 1e9 + 7;
23
24 // Iterate through array, treating each element as potential middle element
25 for (const currentNum of nums) {
26 // Remove current element from right side (as we're now at this position)
27 rightFrequency.set(currentNum, (rightFrequency.get(currentNum) || 0) - 1);
28
29 // Calculate target value (elements that would form valid triplet with current)
30 const targetValue = currentNum * 2;
31
32 // Count occurrences of target value on left side
33 const leftCount = leftFrequency.get(targetValue) || 0;
34
35 // Count occurrences of target value on right side
36 const rightCount = rightFrequency.get(targetValue) || 0;
37
38 // Add number of valid triplets with current element as middle
39 // (leftCount * rightCount gives all possible combinations)
40 tripletCount = (tripletCount + ((leftCount * rightCount) % MOD)) % MOD;
41
42 // Add current element to left side for next iteration
43 leftFrequency.set(currentNum, (leftFrequency.get(currentNum) || 0) + 1);
44 }
45
46 return tripletCount;
47}
48
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
. The algorithm iterates through the array once with a single for loop. Within each iteration, all operations are constant time: decrementing a counter value, performing arithmetic operations and modulo calculations, accessing hash map values, and incrementing a counter value. Since Counter operations (access, increment, decrement) are O(1)
on average, the overall time complexity is linear.
The space complexity is O(n)
, where n
is the length of the array nums
. The algorithm uses two Counter objects: left
and right
. In the worst case where all elements in nums
are unique, left
will eventually contain all n
distinct elements (built up one by one), and right
initially contains all n
distinct elements. Therefore, the total space used by both Counters is O(n)
. The additional variables ans
and mod
use constant space O(1)
, which doesn't affect the overall space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow When Not Applying Modulo Properly
The Pitfall:
A common mistake is to delay the modulo operation until the very end or apply it incorrectly during intermediate calculations. When multiplying left_counter[double_value] * right_counter[double_value]
, this product can exceed the integer limits before applying modulo, especially with large input arrays where frequencies can be high.
Incorrect approach:
# WRONG - might overflow before modulo result += left_counter[double_value] * right_counter[double_value] result = result % MOD # Applied too late
Correct approach:
# Apply modulo to the product immediately triplet_count = (left_counter[double_value] * right_counter[double_value]) % MOD result = (result + triplet_count) % MOD
2. Misunderstanding the Problem Constraints
The Pitfall:
Developers often misinterpret the relationship between the three elements. The problem states that nums[i] == nums[j] * 2
and nums[k] == nums[j] * 2
, which means both the first and third elements should be double the middle element. A common mistake is thinking we need nums[i] * 2 == nums[j]
instead.
Wrong interpretation leads to:
# WRONG - looking for wrong pattern target_value = current_num // 2 # This would find (x/2, x, x/2) pattern triplet_count = left_counter[target_value] * right_counter[target_value]
Correct interpretation:
# Looking for (2x, x, 2x) pattern where x is the middle element double_value = current_num * 2 triplet_count = left_counter[double_value] * right_counter[double_value]
3. Forgetting to Handle Zero or Negative Values
The Pitfall:
If the array contains zeros or negative numbers, special care is needed. When current_num = 0
, then double_value = 0
, which means we're looking for triplets of form (0, 0, 0)
. The counter logic handles this correctly, but developers might add unnecessary special cases that break the solution.
Solution: The current implementation handles all cases uniformly without special-casing, which is the correct approach. The Counter automatically returns 0 for non-existent keys, handling edge cases gracefully.
4. Counter Update Order Mistakes
The Pitfall: The order of updating the left and right counters is crucial. A common mistake is updating them in the wrong sequence:
Wrong order:
# WRONG - adds current element to left before processing left_counter[current_num] += 1 # Should be done AFTER counting right_counter[current_num] -= 1 triplet_count = left_counter[double_value] * right_counter[double_value]
Correct order:
# First remove from right right_counter[current_num] -= 1 # Then count valid triplets triplet_count = left_counter[double_value] * right_counter[double_value] # Finally add to left left_counter[current_num] += 1
This ensures the current element is neither in the left nor right counter when counting triplets with it as the middle element.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!