Facebook Pixel

2179. Count Good Triplets in an Array

Problem Description

You are given two arrays nums1 and nums2, both of length n. Each array is a permutation of [0, 1, ..., n - 1], meaning they contain all numbers from 0 to n-1 exactly once but in different orders.

A good triplet is a set of three distinct values (x, y, z) that satisfy both of these conditions:

  1. In nums1, value x appears before value y, and value y appears before value z
  2. In nums2, value x appears before value y, and value y appears before value z

In other words, if we denote:

  • pos1[v] = index of value v in nums1
  • pos2[v] = index of value v in nums2

Then for a good triplet (x, y, z), we need:

  • pos1[x] < pos1[y] < pos1[z] (x comes before y, y comes before z in nums1)
  • pos2[x] < pos2[y] < pos2[z] (x comes before y, y comes before z in nums2)

The task is to count the total number of such good triplets.

For example, if nums1 = [2, 0, 1] and nums2 = [0, 1, 2]:

  • The triplet (0, 1, 2) is NOT good because in nums1, the order is 2→0→1, so 2 comes before 0 and 1, which violates the required ordering
  • No good triplets exist in this example

The key insight is that a good triplet requires the same relative ordering of three values in both arrays - they must appear in increasing positional order in both permutations.

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

Intuition

Instead of finding all possible triplets directly (which would be O(n³)), let's think about this problem differently. For each element, we can consider it as the middle element of a potential good triplet.

For a value y to be the middle element of a good triplet (x, y, z):

  • Value x must appear before y in both arrays
  • Value z must appear after y in both arrays

The key insight is that as we traverse nums1 from left to right, we're processing elements in their nums1 order. For the current element y:

  • Any element we've already seen comes before y in nums1 (by definition of our traversal)
  • Any element we haven't seen yet comes after y in nums1

Now we need to check the nums2 constraint. Among the elements we've already seen (which are before y in nums1), how many are also before y in nums2? These can serve as x in our triplet. Similarly, among the elements we haven't seen yet (which are after y in nums1), how many are also after y in nums2? These can serve as z.

If there are left valid choices for x and right valid choices for z, then the number of good triplets with y as the middle element is left × right.

To efficiently track which positions in nums2 have been "seen" and quickly count how many seen elements are before a given position, we use a Binary Indexed Tree (Fenwick Tree). As we process each element:

  1. Find its position p in nums2
  2. Count how many already-processed elements have positions less than p in nums2 (these are valid x values)
  3. Count how many unprocessed elements have positions greater than p in nums2 (these are valid z values)
  4. Multiply these counts to get triplets with current element as middle
  5. Mark position p as processed in our Binary Indexed Tree

This reduces the complexity from O(n³) to O(n log n).

Learn more about Segment Tree, Binary Search, Divide and Conquer and Merge Sort patterns.

Solution Approach

The solution uses a Binary Indexed Tree (Fenwick Tree) to efficiently track and query the positions of processed elements in nums2.

Step-by-step Implementation:

  1. Build position mapping: First, create a dictionary pos that maps each value to its 1-indexed position in nums2:

    pos = {v: i for i, v in enumerate(nums2, 1)}

    We use 1-indexed positions because Binary Indexed Trees typically work with 1-based indexing.

  2. Initialize the Binary Indexed Tree: Create a tree of size n to track which positions in nums2 have been processed:

    tree = BinaryIndexedTree(n)

    Initially, all positions have value 0 (unprocessed).

  3. Process each element in nums1: For each number in nums1, we calculate how many good triplets have this number as the middle element:

    for num in nums1:
        p = pos[num]  # Get position of num in nums2
  4. Count valid left elements: Query how many processed elements appear before position p in nums2:

    left = tree.query(p)

    These are elements that:

    • Appear before num in nums1 (already processed)
    • Appear before num in nums2 (position < p)
  5. Count valid right elements: Calculate how many unprocessed elements appear after position p in nums2:

    right = n - p - (tree.query(n) - tree.query(p))

    Breaking this down:

    • n - p: Total elements after position p
    • tree.query(n) - tree.query(p): Processed elements after position p
    • The difference gives us unprocessed elements after p
  6. Calculate triplets with current element as middle:

    ans += left * right

    Each valid left element can pair with each valid right element.

  7. Mark current position as processed:

    tree.update(p, 1)

    Set position p in the tree to 1, indicating this position is now processed.

Binary Indexed Tree Operations:

The Binary Indexed Tree supports two key operations in O(log n) time:

  • query(x): Returns the sum of elements from position 1 to x. This tells us how many positions ≤ x have been processed.

  • update(x, delta): Adds delta to the value at position x. We use delta = 1 to mark a position as processed.

The lowbit(x) function (x & -x) is used internally to navigate the tree structure efficiently.

Example Walkthrough:

Consider nums1 = [4,0,1,3,2] and nums2 = [4,1,0,2,3]:

  • Process 4 (position 1 in nums2): left=0, right=4, contributes 0 triplets
  • Process 0 (position 3 in nums2): left=1, right=2, contributes 2 triplets
  • Process 1 (position 2 in nums2): left=1, right=2, contributes 2 triplets
  • And so on...

The time complexity is O(n log n) due to the Binary Indexed Tree operations, and space complexity is O(n) for the tree and position mapping.

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 a small example with nums1 = [0, 3, 1, 2] and nums2 = [1, 0, 3, 2].

Step 1: Build position mapping for nums2

  • pos = {1: 1, 0: 2, 3: 3, 2: 4} (1-indexed positions)

Step 2: Initialize Binary Indexed Tree

  • Tree starts with all zeros: [0, 0, 0, 0]

Step 3: Process each element in nums1

Processing element 0 (first in nums1):

  • Position in nums2: p = pos[0] = 2
  • Left count: tree.query(2) = 0 (no elements processed yet before position 2)
  • Right count: 4 - 2 - (0 - 0) = 2 (elements at positions 3 and 4 are unprocessed and after position 2)
  • Triplets with 0 as middle: 0 × 2 = 0
  • Update tree at position 2: Tree becomes [0, 1, 0, 0] (conceptually)
  • Running total: 0

Processing element 3 (second in nums1):

  • Position in nums2: p = pos[3] = 3
  • Left count: tree.query(3) = 1 (element 0 at position 2 is processed and before position 3)
  • Right count: 4 - 3 - (1 - 1) = 1 (element at position 4 is unprocessed and after position 3)
  • Triplets with 3 as middle: 1 × 1 = 1 (the triplet is (0, 3, 2))
  • Update tree at position 3: Tree marks position 3 as processed
  • Running total: 1

Processing element 1 (third in nums1):

  • Position in nums2: p = pos[1] = 1
  • Left count: tree.query(1) = 0 (no processed elements before position 1)
  • Right count: 4 - 1 - (2 - 0) = 1 (element 2 at position 4 is unprocessed and after position 1)
  • Triplets with 1 as middle: 0 × 1 = 0
  • Update tree at position 1: Tree marks position 1 as processed
  • Running total: 1

Processing element 2 (fourth in nums1):

  • Position in nums2: p = pos[2] = 4
  • Left count: tree.query(4) = 3 (elements 1, 0, and 3 at positions 1, 2, 3 are all processed and before position 4)
  • Right count: 4 - 4 - (3 - 3) = 0 (no elements after position 4)
  • Triplets with 2 as middle: 3 × 0 = 0
  • Update tree at position 4: Tree marks position 4 as processed
  • Running total: 1

Final Answer: 1 good triplet

The single good triplet is (0, 3, 2):

  • In nums1: 0 appears at index 0, 3 at index 1, 2 at index 3 (0 < 1 < 3) ✓
  • In nums2: 0 appears at index 1, 3 at index 2, 2 at index 3 (1 < 2 < 3) ✓

Solution Implementation

1from typing import List
2
3
4class BinaryIndexedTree:
5    """
6    Binary Indexed Tree (Fenwick Tree) for efficient prefix sum queries and updates.
7    Supports O(log n) update and query operations.
8    """
9  
10    def __init__(self, n: int) -> None:
11        """
12        Initialize a Binary Indexed Tree with size n.
13      
14        Args:
15            n: The size of the tree (1-indexed)
16        """
17        self.size = n
18        # Tree array with 1-based indexing (index 0 is unused)
19        self.tree = [0] * (n + 1)
20  
21    @staticmethod
22    def lowbit(x: int) -> int:
23        """
24        Get the lowest set bit of x using bit manipulation.
25        This determines the range of responsibility for each node.
26      
27        Args:
28            x: The input number
29          
30        Returns:
31            The value of the lowest set bit
32        """
33        return x & -x
34  
35    def update(self, x: int, delta: int) -> None:
36        """
37        Add delta to the value at position x and update all affected nodes.
38      
39        Args:
40            x: The position to update (1-indexed)
41            delta: The value to add at position x
42        """
43        # Traverse up the tree updating all affected nodes
44        while x <= self.size:
45            self.tree[x] += delta
46            # Move to the next node that this update affects
47            x += BinaryIndexedTree.lowbit(x)
48  
49    def query(self, x: int) -> int:
50        """
51        Calculate the prefix sum from index 1 to x (inclusive).
52      
53        Args:
54            x: The ending position for the prefix sum (1-indexed)
55          
56        Returns:
57            The sum of elements from index 1 to x
58        """
59        prefix_sum = 0
60        # Traverse down the tree accumulating partial sums
61        while x > 0:
62            prefix_sum += self.tree[x]
63            # Move to the previous range
64            x -= BinaryIndexedTree.lowbit(x)
65        return prefix_sum
66
67
68class Solution:
69    def goodTriplets(self, nums1: List[int], nums2: List[int]) -> int:
70        """
71        Count the number of good triplets (i, j, k) where i < j < k and
72        the relative order of nums1[i], nums1[j], nums1[k] is the same
73        in both arrays.
74      
75        Args:
76            nums1: First array of distinct integers
77            nums2: Second array containing the same integers as nums1
78          
79        Returns:
80            The count of good triplets
81        """
82        # Map each value to its position in nums2 (1-indexed for BIT)
83        value_to_position = {value: index for index, value in enumerate(nums2, 1)}
84      
85        triplet_count = 0
86        array_length = len(nums1)
87      
88        # Initialize Binary Indexed Tree to track processed elements
89        fenwick_tree = BinaryIndexedTree(array_length)
90      
91        # Process each element in nums1
92        for current_value in nums1:
93            # Get the position of current value in nums2
94            position_in_nums2 = value_to_position[current_value]
95          
96            # Count elements that appear before current position in nums2
97            # and have already been processed (appear before in nums1)
98            elements_before = fenwick_tree.query(position_in_nums2)
99          
100            # Count elements that appear after current position in nums2
101            # and haven't been processed yet (will appear after in nums1)
102            total_processed = fenwick_tree.query(array_length)
103            elements_after_processed = total_processed - fenwick_tree.query(position_in_nums2)
104            elements_after = array_length - position_in_nums2 - elements_after_processed
105          
106            # Each combination of (element before, current element, element after)
107            # forms a valid triplet
108            triplet_count += elements_before * elements_after
109          
110            # Mark current position as processed in the tree
111            fenwick_tree.update(position_in_nums2, 1)
112      
113        return triplet_count
114
1class Solution {
2    public long goodTriplets(int[] nums1, int[] nums2) {
3        int n = nums1.length;
4      
5        // Store the position (1-indexed) of each element in nums2
6        int[] positionInNums2 = new int[n];
7        for (int i = 0; i < n; i++) {
8            positionInNums2[nums2[i]] = i + 1;  // 1-indexed for BIT
9        }
10      
11        // Binary Indexed Tree to track processed elements
12        BinaryIndexedTree fenwickTree = new BinaryIndexedTree(n);
13      
14        long totalGoodTriplets = 0;
15      
16        // Process each element in nums1
17        for (int currentNum : nums1) {
18            // Get the position of current number in nums2 (1-indexed)
19            int currentPos = positionInNums2[currentNum];
20          
21            // Count elements that appear before currentPos and have been processed
22            // These form valid left elements for triplets
23            long validLeftCount = fenwickTree.query(currentPos);
24          
25            // Count elements that appear after currentPos and haven't been processed yet
26            // Total processed = fenwickTree.query(n)
27            // Processed after currentPos = fenwickTree.query(n) - fenwickTree.query(currentPos)
28            // Unprocessed after currentPos = total after - processed after
29            long validRightCount = n - currentPos - (fenwickTree.query(n) - fenwickTree.query(currentPos));
30          
31            // Each combination of valid left and right elements forms a good triplet
32            totalGoodTriplets += validLeftCount * validRightCount;
33          
34            // Mark current position as processed
35            fenwickTree.update(currentPos, 1);
36        }
37      
38        return totalGoodTriplets;
39    }
40}
41
42class BinaryIndexedTree {
43    private int size;
44    private int[] tree;
45  
46    /**
47     * Initialize a Binary Indexed Tree with given size
48     * @param size the size of the tree
49     */
50    public BinaryIndexedTree(int size) {
51        this.size = size;
52        this.tree = new int[size + 1];  // 1-indexed array
53    }
54  
55    /**
56     * Update the value at position x by adding delta
57     * @param x the position to update (1-indexed)
58     * @param delta the value to add
59     */
60    public void update(int x, int delta) {
61        // Propagate the update to all affected nodes
62        while (x <= size) {
63            tree[x] += delta;
64            x += lowbit(x);  // Move to next node that needs updating
65        }
66    }
67  
68    /**
69     * Query the prefix sum from index 1 to x
70     * @param x the end position of the range (1-indexed)
71     * @return the prefix sum
72     */
73    public int query(int x) {
74        int sum = 0;
75        // Accumulate values from relevant nodes
76        while (x > 0) {
77            sum += tree[x];
78            x -= lowbit(x);  // Move to parent node
79        }
80        return sum;
81    }
82  
83    /**
84     * Get the lowest bit of x (rightmost set bit)
85     * @param x the input number
86     * @return the value with only the lowest bit set
87     */
88    public static int lowbit(int x) {
89        return x & -x;
90    }
91}
92
1class BinaryIndexedTree {
2public:
3    int size;                  // Size of the array
4    vector<int> tree;          // Binary indexed tree array (1-indexed)
5
6    // Constructor: Initialize BIT with given size
7    BinaryIndexedTree(int n) : size(n), tree(n + 1, 0) {}
8
9    // Update: Add delta to position x (1-indexed)
10    void update(int x, int delta) {
11        while (x <= size) {
12            tree[x] += delta;
13            x += lowbit(x);    // Move to next responsible node
14        }
15    }
16
17    // Query: Get prefix sum from 1 to x (inclusive)
18    int query(int x) {
19        int sum = 0;
20        while (x > 0) {
21            sum += tree[x];
22            x -= lowbit(x);    // Move to parent node
23        }
24        return sum;
25    }
26
27    // Get the lowest set bit of x
28    int lowbit(int x) {
29        return x & -x;
30    }
31};
32
33class Solution {
34public:
35    long long goodTriplets(vector<int>& nums1, vector<int>& nums2) {
36        int n = nums1.size();
37      
38        // Map each value to its position in nums2 (1-indexed)
39        vector<int> positionInNums2(n);
40        for (int i = 0; i < n; ++i) {
41            positionInNums2[nums2[i]] = i + 1;
42        }
43      
44        // Initialize Binary Indexed Tree to track processed elements
45        BinaryIndexedTree* bit = new BinaryIndexedTree(n);
46        long long totalGoodTriplets = 0;
47      
48        // Process each element in nums1
49        for (int& value : nums1) {
50            // Get position of current value in nums2
51            int currentPos = positionInNums2[value];
52          
53            // Count elements before currentPos that have been processed
54            // These form valid left elements for triplets
55            int validLeftCount = bit->query(currentPos);
56          
57            // Count elements after currentPos that haven't been processed yet
58            // Total after currentPos - Already processed after currentPos
59            int validRightCount = n - currentPos - (bit->query(n) - bit->query(currentPos));
60          
61            // Each combination of valid left and right elements forms a good triplet
62            totalGoodTriplets += static_cast<long long>(validLeftCount) * validRightCount;
63          
64            // Mark current position as processed
65            bit->update(currentPos, 1);
66        }
67      
68        return totalGoodTriplets;
69    }
70};
71
1// Binary Indexed Tree (Fenwick Tree) array for efficient range queries
2let c: number[];
3let n: number;
4
5/**
6 * Calculate the lowest bit of x (rightmost set bit)
7 * Used to navigate the BIT structure
8 */
9function lowbit(x: number): number {
10    return x & -x;
11}
12
13/**
14 * Initialize the Binary Indexed Tree with given size
15 */
16function initBIT(size: number): void {
17    n = size;
18    c = Array(size + 1).fill(0);
19}
20
21/**
22 * Update the BIT by adding delta to position x
23 * Propagates the update to all affected nodes in the tree
24 */
25function update(x: number, delta: number): void {
26    while (x <= n) {
27        c[x] += delta;
28        x += lowbit(x);
29    }
30}
31
32/**
33 * Query the prefix sum from index 1 to x
34 * Returns the sum of elements from position 1 to position x
35 */
36function query(x: number): number {
37    let sum = 0;
38    while (x > 0) {
39        sum += c[x];
40        x -= lowbit(x);
41    }
42    return sum;
43}
44
45/**
46 * Count the number of good triplets (i, j, k) where:
47 * - 0 <= i < j < k < n
48 * - nums1[i] < nums1[j] < nums1[k]
49 * - nums2 positions follow: pos2[nums1[i]] < pos2[nums1[j]] < pos2[nums1[k]]
50 */
51function goodTriplets(nums1: number[], nums2: number[]): number {
52    const arrayLength = nums1.length;
53  
54    // Map each value in nums2 to its 1-indexed position
55    const positionMap = new Map<number, number>();
56    nums2.forEach((value, index) => positionMap.set(value, index + 1));
57
58    // Initialize Binary Indexed Tree with array length
59    initBIT(arrayLength);
60    let result = 0;
61
62    // Process each number in nums1
63    for (const currentNum of nums1) {
64        // Get the position of current number in nums2 (1-indexed)
65        const currentPos = positionMap.get(currentNum)!;
66      
67        // Count how many processed numbers have positions less than current position
68        // These can form the first element of a valid triplet
69        const smallerCount = query(currentPos);
70      
71        // Get total processed numbers so far
72        const totalProcessed = query(arrayLength);
73      
74        // Count how many unprocessed numbers have positions greater than current position
75        // These can form the third element of a valid triplet
76        const greaterCount = arrayLength - currentPos - (totalProcessed - smallerCount);
77      
78        // Add the number of valid triplets where current number is the middle element
79        result += smallerCount * greaterCount;
80      
81        // Mark current position as processed in the BIT
82        update(currentPos, 1);
83    }
84
85    return result;
86}
87

Time and Space Complexity

Time Complexity: O(n log n)

The algorithm iterates through each element in nums1 once, performing the following operations for each element:

  • Dictionary lookup for pos[num]: O(1)
  • Binary Indexed Tree query operation tree.query(p): O(log n) - traverses at most log n nodes by repeatedly removing the lowest bit
  • Binary Indexed Tree query operations for calculating right: O(log n) for both tree.query(n) and tree.query(p)
  • Binary Indexed Tree update operation tree.update(p, 1): O(log n) - traverses at most log n nodes by repeatedly adding the lowest bit

Since we perform O(log n) operations for each of the n elements in the array, the total time complexity is O(n log n).

Space Complexity: O(n)

The algorithm uses:

  • Dictionary pos to store positions of elements from nums2: O(n)
  • Binary Indexed Tree with array c of size n + 1: O(n)
  • A few scalar variables (ans, left, right, etc.): O(1)

Therefore, the overall space complexity is O(n).

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

Common Pitfalls

1. Off-by-One Errors with Indexing

One of the most common mistakes is mixing 0-based and 1-based indexing. The Binary Indexed Tree traditionally uses 1-based indexing, while Python arrays use 0-based indexing.

Pitfall Example:

# WRONG: Using 0-based indexing for BIT
pos = {v: i for i, v in enumerate(nums2)}  # 0-indexed positions
tree.update(pos[num], 1)  # BIT expects 1-indexed positions

Solution: Always use 1-based indexing when working with the Binary Indexed Tree:

# CORRECT: Convert to 1-based indexing
pos = {v: i for i, v in enumerate(nums2, 1)}  # Start enumeration from 1

2. Incorrect Calculation of Elements After Current Position

A subtle but critical error occurs when calculating how many unprocessed elements appear after the current position in nums2.

Pitfall Example:

# WRONG: Forgetting to subtract already processed elements
right = n - p  # This counts ALL elements after p, including processed ones

Solution: Account for already processed elements that appear after the current position:

# CORRECT: Subtract processed elements after position p
right = n - p - (tree.query(n) - tree.query(p))

3. Processing Elements in Wrong Order

The algorithm depends on processing elements in the order they appear in nums1. Processing them in any other order will produce incorrect results.

Pitfall Example:

# WRONG: Sorting or reordering nums1 before processing
for num in sorted(nums1):  # This breaks the algorithm logic
    # Process num...

Solution: Always iterate through nums1 in its original order:

# CORRECT: Process in the exact order of nums1
for num in nums1:
    # Process num...

4. Updating BIT Before Counting Triplets

The order of operations matters. If you update the tree before counting, you'll include the current element in your own count.

Pitfall Example:

# WRONG: Update before counting
for num in nums1:
    p = pos[num]
    tree.update(p, 1)  # Updated too early!
    left = tree.query(p)  # Now includes current element
    right = n - p - (tree.query(n) - tree.query(p))
    ans += left * right

Solution: Always count first, then update:

# CORRECT: Count first, then update
for num in nums1:
    p = pos[num]
    left = tree.query(p)  # Count elements before
    right = n - p - (tree.query(n) - tree.query(p))  # Count elements after
    ans += left * right
    tree.update(p, 1)  # Update after counting

5. Misunderstanding the Problem Requirements

A common conceptual error is misunderstanding what constitutes a "good triplet". The triplet consists of three VALUES (not indices) that must maintain the same relative order in both arrays.

Pitfall Example:

# WRONG: Thinking about index triplets instead of value triplets
for i in range(n):
    for j in range(i+1, n):
        for k in range(j+1, n):
            # This is checking index relationships, not value relationships

Solution: Focus on the values themselves and their relative positions in both arrays. The BIT solution correctly handles this by tracking which values have been processed and their positions in nums2.

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

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings

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

Load More