LeetCode 2179 - Count Good Triplets in an Array
In this problem, we are given two arrays, nums1 and nums2, each containing every integer from 0 to n - 1 exactly once. In other words, both arrays are permutations of the same set of values.
Difficulty: 🔴 Hard
Topics: Array, Binary Search, Divide and Conquer, Binary Indexed Tree, Segment Tree, Merge Sort, Ordered Set
Solution
Problem Understanding
In this problem, we are given two arrays, nums1 and nums2, each containing every integer from 0 to n - 1 exactly once. In other words, both arrays are permutations of the same set of values.
We need to count how many triplets (x, y, z) satisfy an ordering condition in both arrays simultaneously.
For a triplet to be considered "good", the following must hold:
- In
nums1, the position ofxmust come beforey, and the position ofymust come beforez - In
nums2, the same ordering must also hold
Formally:
pos1[x] < pos1[y] < pos1[z]pos2[x] < pos2[y] < pos2[z]
The important detail is that the triplet consists of values, not indices. We are comparing where those values appear inside the two arrays.
The constraints are large:
ncan be as large as100000- A brute force enumeration of all triplets would require checking approximately
n^3combinations
Since 100000^3 is astronomically large, any cubic or even quadratic solution is too slow. This strongly suggests that we need an efficient counting technique using advanced data structures such as:
- Binary Indexed Tree
- Segment Tree
- Ordered statistics
- Merge sort counting
Another important observation is that both arrays are permutations. This means every value appears exactly once in each array. Because of this, we can map values from one array into positions in the other array, transforming the problem into an order counting problem on a single array.
Some edge cases that matter:
- Arrays that are already in the same order, every increasing triplet is valid
- Arrays in nearly opposite order, very few or zero triplets exist
- Smallest valid input size
n = 3 - Large inputs where performance becomes critical
- Cases where one middle element has many valid left elements but few right elements, or vice versa
The permutation guarantee simplifies many implementation details because we never need to handle duplicates.
Approaches
Brute Force
The most direct approach is to enumerate every possible triplet (x, y, z) and verify whether it appears in increasing order in both arrays.
We can first build two position maps:
pos1[value] = index in nums1pos2[value] = index in nums2
Then for every combination of three distinct values, we check:
pos1[x] < pos1[y] < pos1[z]
and
pos2[x] < pos2[y] < pos2[z]
If both conditions hold, we increment the answer.
This works because it directly follows the problem definition. However, there are O(n^3) triplets, which is completely infeasible for n = 100000.
Key Insight
The important observation is that we only care about relative ordering.
Suppose we map every value in nums1 to its position in nums2.
Example:
nums1 = [2,0,1,3]
nums2 = [0,1,2,3]
Positions in nums2:
0 -> 0
1 -> 1
2 -> 2
3 -> 3
Transform nums1:
[2,0,1,3]
->
[2,0,1,3]
Now the problem becomes:
Count increasing subsequences of length 3 in this transformed array.
Why does this work?
- The order in
nums1is already fixed by iterating through the transformed array - If indices in the transformed array are increasing, then the corresponding values also appear in increasing order in
nums2
Now we can process each element as the middle element of a triplet:
- Count how many smaller elements exist to its left
- Count how many larger elements exist to its right
- Multiply the two counts
This can be done efficiently with a Binary Indexed Tree in O(n log n) time.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n³) | O(n) | Checks every triplet explicitly |
| Optimal | O(n log n) | O(n) | Uses permutation mapping and Binary Indexed Tree |
Algorithm Walkthrough
Step 1: Build Position Mapping
Create an array position_in_nums2 where:
position_in_nums2[value] = index of value in nums2
This allows us to instantly determine where each value appears in nums2.
For example:
nums2 = [4,1,0,2,3]
position_in_nums2:
0 -> 2
1 -> 1
2 -> 3
3 -> 4
4 -> 0
Step 2: Transform nums1
Replace every value in nums1 with its position in nums2.
Example:
nums1 = [4,0,1,3,2]
transformed:
[0,2,1,4,3]
Now the problem becomes:
Count increasing subsequences of length 3 in this transformed array.
Why?
Because:
- The index order in the transformed array already represents order in
nums1 - Increasing values inside the transformed array represent increasing order in
nums2
Step 3: Count Smaller Elements on the Left
We process the transformed array from left to right.
For each position i, we want:
left_smaller[i]
=
number of previous elements smaller than transformed[i]
We use a Binary Indexed Tree to maintain frequencies of previously seen values.
For each value:
- Query how many smaller values have appeared
- Store the result
- Insert the current value into the BIT
The Binary Indexed Tree supports:
- Prefix sum query in
O(log n) - Point update in
O(log n)
Step 4: Count Larger Elements on the Right
Now process from right to left.
For each position i, we want:
right_larger[i]
=
number of later elements larger than transformed[i]
Again use a Binary Indexed Tree.
At each step:
- Query how many elements already inserted are larger
- Store the result
- Insert current value
Step 5: Combine Counts
Treat every position as the middle element of a triplet.
If:
left_smaller[i] = aright_larger[i] = b
then there are:
a * b
good triplets centered at position i.
Sum over all positions.
Why it works
After transformation, the array represents the order of elements in nums2 while preserving the order from nums1.
An increasing subsequence of length 3 in the transformed array corresponds exactly to three values that appear in increasing order in both arrays.
For each middle element:
- Any valid left element must be smaller
- Any valid right element must be larger
Every pair of such choices forms a unique valid triplet, so multiplying the counts gives the exact number of triplets centered at that position.
Python Solution
from typing import List
class BIT:
def __init__(self, size: int):
self.size = size
self.tree = [0] * (size + 1)
def update(self, index: int, delta: int) -> None:
index += 1
while index <= self.size:
self.tree[index] += delta
index += index & -index
def query(self, index: int) -> int:
index += 1
result = 0
while index > 0:
result += self.tree[index]
index -= index & -index
return result
class Solution:
def goodTriplets(self, nums1: List[int], nums2: List[int]) -> int:
n = len(nums1)
position_in_nums2 = [0] * n
for index, value in enumerate(nums2):
position_in_nums2[value] = index
transformed = [position_in_nums2[value] for value in nums1]
left_smaller = [0] * n
bit = BIT(n)
for i in range(n):
value = transformed[i]
left_smaller[i] = bit.query(value - 1)
bit.update(value, 1)
right_larger = [0] * n
bit = BIT(n)
for i in range(n - 1, -1, -1):
value = transformed[i]
total_seen = bit.query(n - 1)
smaller_or_equal = bit.query(value)
right_larger[i] = total_seen - smaller_or_equal
bit.update(value, 1)
answer = 0
for i in range(n):
answer += left_smaller[i] * right_larger[i]
return answer
The implementation starts by constructing a position lookup table for nums2. Since both arrays are permutations, every value has exactly one position.
Next, the code transforms nums1 into an array of positions inside nums2. This reduces the original problem into counting increasing subsequences of length 3.
The BIT class implements a Binary Indexed Tree, also known as a Fenwick Tree. It supports efficient prefix sum queries and point updates in logarithmic time.
The first pass computes left_smaller. For each element, the BIT stores all previously seen values. Querying value - 1 tells us how many smaller elements appear before the current element.
The second pass computes right_larger. We traverse from right to left and maintain another BIT containing elements already seen on the right side. The difference:
total_seen - smaller_or_equal
gives the number of strictly larger elements to the right.
Finally, each index is treated as the middle element of a triplet. Multiplying the number of valid left choices and right choices gives the total contribution for that position.
Go Solution
package main
type BIT struct {
tree []int
size int
}
func NewBIT(size int) *BIT {
return &BIT{
tree: make([]int, size+1),
size: size,
}
}
func (b *BIT) Update(index int, delta int) {
index++
for index <= b.size {
b.tree[index] += delta
index += index & -index
}
}
func (b *BIT) Query(index int) int {
index++
result := 0
for index > 0 {
result += b.tree[index]
index -= index & -index
}
return result
}
func goodTriplets(nums1 []int, nums2 []int) int64 {
n := len(nums1)
positionInNums2 := make([]int, n)
for index, value := range nums2 {
positionInNums2[value] = index
}
transformed := make([]int, n)
for i, value := range nums1 {
transformed[i] = positionInNums2[value]
}
leftSmaller := make([]int, n)
bit := NewBIT(n)
for i := 0; i < n; i++ {
value := transformed[i]
leftSmaller[i] = bit.Query(value - 1)
bit.Update(value, 1)
}
rightLarger := make([]int, n)
bit = NewBIT(n)
for i := n - 1; i >= 0; i-- {
value := transformed[i]
totalSeen := bit.Query(n - 1)
smallerOrEqual := bit.Query(value)
rightLarger[i] = totalSeen - smallerOrEqual
bit.Update(value, 1)
}
var answer int64 = 0
for i := 0; i < n; i++ {
answer += int64(leftSmaller[i] * rightLarger[i])
}
return answer
}
The Go implementation follows the same algorithmic structure as the Python version.
One important difference is the return type. The problem requires returning int64 because the number of triplets can exceed the range of a 32-bit integer.
The Binary Indexed Tree is implemented as a struct with methods for updates and prefix queries. Go slices are used instead of Python lists, but the logic remains identical.
Another detail is explicit integer conversion when accumulating the final answer:
answer += int64(leftSmaller[i] * rightLarger[i])
Without this conversion, integer overflow could occur on some platforms.
Worked Examples
Example 1
nums1 = [2,0,1,3]
nums2 = [0,1,2,3]
Step 1: Build Position Map
| Value | Position in nums2 |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
Step 2: Transform nums1
nums1:
[2,0,1,3]
transformed:
[2,0,1,3]
Step 3: Compute left_smaller
| i | value | smaller on left | left_smaller |
|---|---|---|---|
| 0 | 2 | 0 | 0 |
| 1 | 0 | 0 | 0 |
| 2 | 1 | 1 | 1 |
| 3 | 3 | 3 | 3 |
Result:
left_smaller = [0,0,1,3]
Step 4: Compute right_larger
| i | value | larger on right | right_larger |
|---|---|---|---|
| 3 | 3 | 0 | 0 |
| 2 | 1 | 1 | 1 |
| 1 | 0 | 2 | 2 |
| 0 | 2 | 1 | 1 |
Result:
right_larger = [1,2,1,0]
Step 5: Combine
| i | left_smaller | right_larger | contribution |
|---|---|---|---|
| 0 | 0 | 1 | 0 |
| 1 | 0 | 2 | 0 |
| 2 | 1 | 1 | 1 |
| 3 | 3 | 0 | 0 |
Total:
1
Example 2
nums1 = [4,0,1,3,2]
nums2 = [4,1,0,2,3]
Position Map
| Value | Position |
|---|---|
| 0 | 2 |
| 1 | 1 |
| 2 | 3 |
| 3 | 4 |
| 4 | 0 |
Transformed Array
[0,2,1,4,3]
left_smaller
| i | value | left_smaller |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 2 | 1 |
| 2 | 1 | 1 |
| 3 | 4 | 3 |
| 4 | 3 | 3 |
right_larger
| i | value | right_larger |
|---|---|---|
| 4 | 3 | 0 |
| 3 | 4 | 0 |
| 2 | 1 | 2 |
| 1 | 2 | 2 |
| 0 | 0 | 4 |
Contributions
| i | left | right | contribution |
|---|---|---|---|
| 0 | 0 | 4 | 0 |
| 1 | 1 | 2 | 2 |
| 2 | 1 | 2 | 2 |
| 3 | 3 | 0 | 0 |
| 4 | 3 | 0 | 0 |
Total:
4
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Each BIT query and update takes O(log n), performed O(n) times |
| Space | O(n) | Position map, transformed array, BIT, and helper arrays |
The Binary Indexed Tree supports both update and prefix sum operations in logarithmic time. Since we perform a constant number of these operations for every element, the overall runtime becomes O(n log n).
The auxiliary memory usage is linear because we store several arrays of size n.
Test Cases
from typing import List
class Solution:
def goodTriplets(self, nums1: List[int], nums2: List[int]) -> int:
n = len(nums1)
pos = [0] * n
for i, v in enumerate(nums2):
pos[v] = i
arr = [pos[v] for v in nums1]
class BIT:
def __init__(self, size):
self.tree = [0] * (size + 1)
self.size = size
def update(self, index, delta):
index += 1
while index <= self.size:
self.tree[index] += delta
index += index & -index
def query(self, index):
index += 1
result = 0
while index > 0:
result += self.tree[index]
index -= index & -index
return result
left = [0] * n
bit = BIT(n)
for i, v in enumerate(arr):
left[i] = bit.query(v - 1)
bit.update(v, 1)
right = [0] * n
bit = BIT(n)
for i in range(n - 1, -1, -1):
v = arr[i]
right[i] = bit.query(n - 1) - bit.query(v)
bit.update(v, 1)
answer = 0
for i in range(n):
answer += left[i] * right[i]
return answer
solver = Solution()
assert solver.goodTriplets(
[2,0,1,3],
[0,1,2,3]
) == 1 # official example 1
assert solver.goodTriplets(
[4,0,1,3,2],
[4,1,0,2,3]
) == 4 # official example 2
assert solver.goodTriplets(
[0,1,2],
[0,1,2]
) == 1 # smallest size with one valid triplet
assert solver.goodTriplets(
[0,1,2],
[2,1,0]
) == 0 # completely reversed ordering
assert solver.goodTriplets(
[0,1,2,3],
[0,1,2,3]
) == 4 # every triplet is valid
assert solver.goodTriplets(
[3,2,1,0],
[0,1,2,3]
) == 0 # no increasing subsequence of length 3
assert solver.goodTriplets(
[1,0,2,3],
[0,1,2,3]
) == 2 # mixed ordering
assert solver.goodTriplets(
[0,2,1,3],
[0,1,2,3]
) == 2 # partial inversion
assert solver.goodTriplets(
[2,1,0,3,4],
[0,1,2,3,4]
) == 4 # only later elements form triplets
| Test | Why |
|---|---|
[2,0,1,3] and [0,1,2,3] |
Verifies official example |
[4,0,1,3,2] and [4,1,0,2,3] |
Verifies official example |
[0,1,2] and [0,1,2] |
Smallest valid input with one triplet |
[0,1,2] and [2,1,0] |
No valid triplets |
| Identical sorted arrays | Every increasing triplet should count |
| Reverse ordering | Ensures algorithm handles zero answers correctly |
| Partial inversions | Verifies mixed ordering behavior |
| Larger mixed case | Stresses counting logic |
Edge Cases
One important edge case occurs when both arrays are identical and strictly increasing. In this scenario, every possible combination of three indices forms a valid triplet. This can produce very large answers, especially for large n. The implementation handles this correctly because the Binary Indexed Tree efficiently counts all increasing subsequences, and the Go solution uses int64 to avoid overflow.
Another important case is when the arrays are effectively reversed relative to each other. For example:
nums1 = [0,1,2]
nums2 = [2,1,0]
In this situation, no increasing triplet can exist because every ordering in one array becomes decreasing in the other. The implementation naturally handles this because the transformed array contains no increasing subsequences of length three, causing all contributions to become zero.
A subtle edge case involves middle elements that have many smaller values on the left but no larger values on the right, or vice versa. Since the algorithm multiplies the two counts, any missing side correctly produces zero contribution. This prevents invalid partial triplets from being counted.
Another potential source of bugs is off by one indexing inside the Binary Indexed Tree. The implementation carefully converts zero based array indices into one based BIT indices internally by incrementing the index before updates and queries. This ensures prefix sums are computed correctly for all values from 0 to n - 1.