Facebook Pixel

3583. Count Special Triplets

MediumArrayHash TableCounting
Leetcode Link

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:

  1. The indices must be in strictly increasing order: 0 <= i < j < k < n, where n is the length of the array
  2. The element at index i must equal twice the element at index j: nums[i] == nums[j] * 2
  3. The element at index k must also equal twice the element at index j: 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.

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

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 index j (these can be our nums[i])
  • How many elements equal to nums[j] * 2 appear after index j (these can be our nums[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:

  1. We remove it from the right counter (since it's no longer to the right of our current position)
  2. We check how many times nums[j] * 2 appears in both left and right counters
  3. We multiply these counts to get the number of valid triplets with this middle element
  4. 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 position
  • right: A Counter that initially contains all elements and tracks frequencies to the right of the current position

Algorithm Steps:

  1. Initialize the counters:

    • Create an empty left counter
    • Create a right counter initialized with all elements from nums
    • Set ans = 0 to accumulate the result
    • Define mod = 10^9 + 7 for the modulo operation
  2. Iterate through each element as the middle element: For each element x in nums:

    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 to x * 2 before current position)
    • Count of valid k indices: right[x * 2] (elements equal to x * 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
  3. 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 Evaluator

Example 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More