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:
- In
nums1
, valuex
appears before valuey
, and valuey
appears before valuez
- In
nums2
, valuex
appears before valuey
, and valuey
appears before valuez
In other words, if we denote:
pos1[v]
= index of valuev
innums1
pos2[v]
= index of valuev
innums2
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 innums1
, 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.
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 beforey
in both arrays - Value
z
must appear aftery
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
innums1
(by definition of our traversal) - Any element we haven't seen yet comes after
y
innums1
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:
- Find its position
p
innums2
- Count how many already-processed elements have positions less than
p
innums2
(these are validx
values) - Count how many unprocessed elements have positions greater than
p
innums2
(these are validz
values) - Multiply these counts to get triplets with current element as middle
- 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:
-
Build position mapping: First, create a dictionary
pos
that maps each value to its 1-indexed position innums2
: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.
-
Initialize the Binary Indexed Tree: Create a tree of size
n
to track which positions innums2
have been processed:tree = BinaryIndexedTree(n)
Initially, all positions have value 0 (unprocessed).
-
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
-
Count valid left elements: Query how many processed elements appear before position
p
innums2
:left = tree.query(p)
These are elements that:
- Appear before
num
innums1
(already processed) - Appear before
num
innums2
(position <p
)
- Appear before
-
Count valid right elements: Calculate how many unprocessed elements appear after position
p
innums2
:right = n - p - (tree.query(n) - tree.query(p))
Breaking this down:
n - p
: Total elements after positionp
tree.query(n) - tree.query(p)
: Processed elements after positionp
- The difference gives us unprocessed elements after
p
-
Calculate triplets with current element as middle:
ans += left * right
Each valid left element can pair with each valid right element.
-
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 tox
. This tells us how many positions ≤x
have been processed. -
update(x, delta)
: Addsdelta
to the value at positionx
. We usedelta = 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 EvaluatorExample 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 mostlog n
nodes by repeatedly removing the lowest bit - Binary Indexed Tree query operations for calculating
right
:O(log n)
for bothtree.query(n)
andtree.query(p)
- Binary Indexed Tree update operation
tree.update(p, 1)
:O(log n)
- traverses at mostlog 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 fromnums2
:O(n)
- Binary Indexed Tree with array
c
of sizen + 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
.
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
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Want a Structured Path to Master System Design Too? Don’t Miss This!