Count Good Triplets in an Array
HardPrompt
You are given two 0-indexed arrays nums1 and nums2 of length n, both of which are permutations of [0, 1, ..., n - 1].
A good triplet is a set of 3 distinct values which are present in increasing order by position both in nums1 and nums2. In other words, if we consider pos1v as the index of the value v in nums1 and pos2v as the index of the value v in nums2, then a good triplet will be a set (x, y, z) where 0 <= x, y, z <= n - 1, such that pos1x < pos1y < pos1z and pos2x < pos2y < pos2z.
Return the total number of good triplets.
Example 1:
Input: nums1 = [2,0,1,3], nums2 = [0,1,2,3]
Output: 1
Explanation:
There are 4 triplets (x,y,z) such that pos1x < pos1y < pos1z. They are (2,0,1), (2,0,3), (2,1,3), and (0,1,3).
Out of those triplets, only the triplet (0,1,3) satisfies pos2x < pos2y < pos2z. Hence, there is only 1 good triplet.Example 2:
Input: nums1 = [4,0,1,3,2], nums2 = [4,1,0,2,3]
Output: 4
Explanation: The 4 good triplets are (4,0,3), (4,0,2), (4,1,3), and (4,1,2).
Constraints:
n == nums1.length == nums2.length3 <= n <= 1050 <= nums1[i], nums2[i] <= n - 1nums1andnums2are permutations of[0, 1, ..., n - 1].
Approaches
2 approaches with complexity analysis and trade-offs.
The O(n^2) approach is too slow because counting smaller elements to the left and larger elements to the right takes O(n) time for each middle element. This counting process can be optimized using a data structure that supports fast queries and updates.
A Fenwick Tree (or Binary Indexed Tree, BIT) is ideal for this. It can count the number of elements in a range and add a new element in O(log n) time. By processing the elements of the transformed array A and using a BIT, we can calculate the required counts efficiently.
We can either use two passes over the array A—one forward and one backward, each with its own BIT—to compute arrays of smaller_left and larger_right counts. Or, more elegantly, we can devise a single-pass algorithm that calculates the contribution of each element as it iterates, using just one BIT.
Algorithm
- First, perform the same problem transformation as in the brute-force approach. Create
pos2map and the arrayAwhereA[i] = pos2[nums1[i]]. - The goal is to efficiently count, for each
j, the number of smaller elements to the left (smaller_left) and larger elements to the right (larger_right). - Two-Pass Method:
- Calculate
smaller_leftfor allj: Initialize a Fenwick Tree (BIT). Iteratejfrom0ton-1. For eachj,smaller_left[j]is the count of numbers less thanA[j]seen so far, which can be found withBIT.query(A[j]). Then, update the BIT withA[j]by callingBIT.update(A[j] + 1, 1). - Calculate
larger_rightfor allj: Initialize a new BIT. Iteratejfromn-1down to0. For eachj,larger_right[j]is the count of numbers greater thanA[j]seen so far (i.e., to the right). This is(elements processed so far) - (elements <= A[j]), which can be found withBIT.query(n) - BIT.query(A[j] + 1). Then, update the BIT withA[j]. - Finally, iterate from
j=0ton-1and sum up the products(long)smaller_left[j] * larger_right[j].
- Calculate
- Single-Pass Optimization:
- We can compute the total count in a single pass. Initialize one BIT and a
total_tripletscounter. - Iterate
jfrom0ton-1. - In each iteration,
A[j]is the middle element. The elementsA[0...j-1]are already processed and stored in the BIT. smaller_left = BIT.query(A[j]). This is the count of elements seen so far that are smaller thanA[j].larger_rightcan be calculated without a second pass. The total count of numbers in the universe[0...n-1]that are greater thanA[j]is(n - 1 - A[j]). Some of these are on the left ofj, and some are on the right. The count of numbers greater thanA[j]on the left islarger_left = (j) - smaller_left. So,larger_right = (total_greater) - (larger_left) = (n - 1 - A[j]) - (j - smaller_left).- Add
(long)smaller_left * larger_righttototal_triplets. - Update the BIT with
A[j]for future iterations:BIT.update(A[j] + 1, 1).
- We can compute the total count in a single pass. Initialize one BIT and a
Walkthrough
The core idea is to optimize the counting of smaller_left and larger_right for each potential middle element A[j]. After transforming the problem to finding increasing subsequences of length 3 in array A, we can proceed with a single-pass approach using a Fenwick Tree.
We iterate through A from left to right (index j). For each element A[j], we treat it as the middle element of a triplet (A[i], A[j], A[k]) where i < j < k.
-
Count smaller elements on the left (
smaller_left): The elementsA[0], ..., A[j-1]have already been processed. We can use a Fenwick Tree to maintain the counts of these numbers. A query to the BIT,BIT.query(A[j]), will give us the number of elements seen so far that are strictly less thanA[j]. This is oursmaller_leftcount. -
Count larger elements on the right (
larger_right): We need to count elementsA[k]wherek > jandA[k] > A[j]. We can deduce this count without iterating. The total number of values in the permutation greater thanA[j]is(n - 1) - A[j]. These values are distributed to the left and right of indexj. We can find how many are on the left:larger_leftis the total number of elements to the left (j) minus those that are smaller (smaller_left). So,larger_left = j - smaller_left. Therefore,larger_right = (total_values > A[j]) - larger_left = (n - 1 - A[j]) - (j - smaller_left). -
Update total: For the current middle element
A[j], we have foundsmaller_leftandlarger_right. We add their product to our running total. -
Update BIT: After processing
A[j], we must add it to our Fenwick Tree so it's included in the counts for subsequent elements. We do this withBIT.update(A[j] + 1, 1).
This single pass correctly accumulates the total count of good triplets.
class Solution { // Fenwick Tree (Binary Indexed Tree) implementation class FenwickTree { private int[] bit; private int size; public FenwickTree(int size) { this.size = size; this.bit = new int[size + 1]; } public void update(int index, int delta) { while (index <= size) { bit[index] += delta; index += index & -index; // Move to the next relevant index } } public int query(int index) { int sum = 0; while (index > 0) { sum += bit[index]; index -= index & -index; // Move to the parent index } return sum; } } public long goodTriplets(int[] nums1, int[] nums2) { int n = nums1.length; int[] pos2 = new int[n]; for (int i = 0; i < n; ++i) { pos2[nums2[i]] = i; } int[] A = new int[n]; for (int i = 0; i < n; ++i) { A[i] = pos2[nums1[i]]; } // The values in A are 0 to n-1. BIT needs size n+1 for 1-based indexing. FenwickTree bit = new FenwickTree(n); long totalTriplets = 0; for (int j = 0; j < n; ++j) { // A[j] is the middle element. Values are 0-indexed. // Query for elements < A[j]. BIT is 1-indexed, so query up to A[j]. int smaller_left = bit.query(A[j]); // Number of elements processed so far (to the left of j) is j. // Number of elements > A[j] on the left. int larger_left = j - smaller_left; // Total numbers in the permutation > A[j] is (n - 1 - A[j]). // Number of elements > A[j] on the right. int larger_right = (n - 1 - A[j]) - larger_left; totalTriplets += (long) smaller_left * larger_right; // Add current element A[j] to BIT for subsequent calculations. // Update at index A[j] + 1 because BIT is 1-indexed. bit.update(A[j] + 1, 1); } return totalTriplets; }}Complexity
Time
O(n log n). The pre-computation takes O(n). The main loop runs `n` times, and inside the loop, the Fenwick Tree operations (query and update) each take O(log n) time. This results in a total time complexity of O(n log n).
Space
O(n) for the `pos2` map, the transformed array `A`, and the Fenwick Tree.
Trade-offs
Pros
Highly efficient with O(n log n) time complexity, which passes the given constraints.
It's a standard and powerful technique for solving a class of problems related to counting inversions or ordered subsequences.
Cons
Requires knowledge of advanced data structures like Fenwick Trees or Segment Trees.
Implementation is more complex than the brute-force approach.
This approach first simplifies the problem. The condition of having increasing positions in both arrays is complex. We can fix the order of one array, say nums1, and then check the condition on the second. By iterating through nums1, any three values x, y, z picked at indices i < j < k automatically satisfy pos1_x < pos1_y < pos1_z. The problem then reduces to counting how many of these triplets also satisfy pos2_x < pos2_y < pos2_z.
To make this check efficient, we transform nums1 into a new array A where A[i] stores the position of nums1[i] in nums2. Now, the problem is equivalent to finding the number of increasing subsequences of length 3 in A.
The brute-force method iterates through every possible middle element A[j] of a triplet, and for each, it counts the number of smaller elements to its left and the number of larger elements to its right by linear scans.
Algorithm
- Create a mapping
pos2from each value to its index innums2. This allows for O(1) lookup of a value's position innums2. - Create a new array
Aof lengthn, whereA[i] = pos2[nums1[i]]. This transformation simplifies the problem: a triplet of values(x, y, z)is good if and only if their corresponding indicesi, j, kinAsatisfyi < j < kandA[i] < A[j] < A[k]. - The problem is now to count the number of increasing subsequences of length 3 in array
A. - Initialize a
longvariablecountto 0. - Iterate through the array
Awith an indexjfrom1ton-2. ThisA[j]will be the middle element of a potential triplet. - For each
j, we need to find:smaller_left: The number of elementsA[i]withi < jsuch thatA[i] < A[j].larger_right: The number of elementsA[k]withk > jsuch thatA[k] > A[j].
- To find
smaller_left, iterate with an indexifrom0toj-1and count. - To find
larger_right, iterate with an indexkfromj+1ton-1and count. - The number of good triplets with
A[j]as the middle element issmaller_left * larger_right. - Add this product to the total
count. - After the main loop finishes, return
count.
Walkthrough
First, we need a way to quickly find the position of any number in nums2. We can pre-process nums2 and store the indices in a hash map or an array, let's call it pos2. pos2[value] = index.
Next, we create a new array A. For each index i from 0 to n-1, we set A[i] = pos2[nums1[i]]. Now, if we pick three indices i < j < k, the values x=nums1[i], y=nums1[j], z=nums1[k] satisfy the first condition (pos1_x < pos1_y < pos1_z). For these to form a good triplet, they must also satisfy pos2_x < pos2_y < pos2_z, which translates to pos2[nums1[i]] < pos2[nums1[j]] < pos2[nums1[k]], or simply A[i] < A[j] < A[k].
So, the task is to count triplets of indices (i, j, k) such that i < j < k and A[i] < A[j] < A[k]. We can do this by iterating through each possible middle index j from 1 to n-2. For each j, we count how many elements to its left are smaller (smaller_left) and how many elements to its right are larger (larger_right). The number of triplets centered at j is then smaller_left * larger_right. Summing this product over all possible j gives the total count.
class Solution { public long goodTriplets(int[] nums1, int[] nums2) { int n = nums1.length; int[] pos2 = new int[n]; for (int i = 0; i < n; ++i) { pos2[nums2[i]] = i; } int[] A = new int[n]; for (int i = 0; i < n; ++i) { A[i] = pos2[nums1[i]]; } long totalTriplets = 0; for (int j = 1; j < n - 1; ++j) { int smaller_left = 0; for (int i = 0; i < j; ++i) { if (A[i] < A[j]) { smaller_left++; } } int larger_right = 0; for (int k = j + 1; k < n; ++k) { if (A[k] > A[j]) { larger_right++; } } totalTriplets += (long) smaller_left * larger_right; } return totalTriplets; }}Complexity
Time
O(n^2). The pre-computation of `pos2` and `A` takes O(n). The main part involves a nested loop structure. The outer loop for `j` runs `n` times, and for each `j`, the inner loops for `i` and `k` take O(j) and O(n-j) time respectively, leading to an overall O(n^2) complexity.
Space
O(n) to store the `pos2` mapping and the transformed array `A`.
Trade-offs
Pros
Relatively simple to understand and implement.
Improves upon a naive O(n^3) solution by fixing the middle element.
Cons
The time complexity of O(n^2) is too slow for the given constraints where
ncan be up to 10^5, and will result in a 'Time Limit Exceeded' error.
Solutions
Solution
class Solution {public long goodTriplets(int[] nums1, int[] nums2) { int n = nums1.length; int[] pos = new int[n]; BinaryIndexedTree tree = new BinaryIndexedTree(n); for (int i = 0; i < n; ++i) { pos[nums2[i]] = i + 1; } long ans = 0; for (int num : nums1) { int p = pos[num]; long left = tree.query(p); long right = n - p - (tree.query(n) - tree.query(p)); ans += left * right; tree.update(p, 1); } return ans; }} class BinaryIndexedTree {private int n;private int[] c;public BinaryIndexedTree(int n) { this.n = n; c = new int[n + 1]; }public void update(int x, int delta) { while (x <= n) { c[x] += delta; x += lowbit(x); } }public int query(int x) { int s = 0; while (x > 0) { s += c[x]; x -= lowbit(x); } return s; }public static int lowbit(int x) { return x & -x; }}class BinaryIndexedTree { public: int n ; vector < int > c ; BinaryIndexedTree ( int _n ) : n ( _n ) , c ( _n + 1 ) {} void update ( int x , int delta ) { while ( x <= n ) { c [ x ] += delta ; x += lowbit ( x ); } } int query ( int x ) { int s = 0 ; while ( x > 0 ) { s += c [ x ]; x -= lowbit ( x ); } return s ; } int lowbit ( int x ) { return x & - x ; } }; class Solution { public: long long goodTriplets ( vector < int >& nums1 , vector < int >& nums2 ) { int n = nums1 . size (); vector < int > pos ( n ); for ( int i = 0 ; i < n ; ++ i ) pos [ nums2 [ i ]] = i + 1 ; BinaryIndexedTree * tree = new BinaryIndexedTree ( n ); long long ans = 0 ; for ( int & num : nums1 ) { int p = pos [ num ]; int left = tree -> query ( p ); int right = n - p - ( tree -> query ( n ) - tree -> query ( p )); ans += 1ll * left * right ; tree -> update ( p , 1 ); } return ans ; } };class BinaryIndexedTree : def __init__ ( self , n ): self . n = n self . c = [ 0 ] * ( n + 1 ) @ staticmethod def lowbit ( x ): return x & - x def update ( self , x , delta ): while x <= self . n : self . c [ x ] += delta x += BinaryIndexedTree . lowbit ( x ) def query ( self , x ): s = 0 while x > 0 : s += self . c [ x ] x -= BinaryIndexedTree . lowbit ( x ) return s class Solution : def goodTriplets ( self , nums1 : List [ int ], nums2 : List [ int ]) -> int : pos = { v : i for i , v in enumerate ( nums2 , 1 )} ans = 0 n = len ( nums1 ) tree = BinaryIndexedTree ( n ) for num in nums1 : p = pos [ num ] left = tree . query ( p ) right = n - p - ( tree . query ( n ) - tree . query ( p )) ans += left * right tree . update ( p , 1 ) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.