823. Binary Trees With Factors
Problem Description
You are given an array of unique integers where each integer is strictly greater than 1. Your task is to count how many binary trees can be constructed using these integers as node values, following a specific rule: each non-leaf node's value must equal the product of its two children's values.
The key points of this problem are:
- Available numbers: You can use any integer from the given array
arr
as a node value - Reusability: Each number can be used multiple times across different nodes in the tree
- Parent-child relationship: For any non-leaf node with value
parent
, if its children have valuesleft
andright
, thenparent = left × right
- Leaf nodes: These can be any value from the array with no multiplication constraint
- Return value: The total count of valid binary trees modulo
10^9 + 7
For example, if arr = [2, 4]
, you can create three valid trees:
- A single node with value 2
- A single node with value 4
- A tree with root 4 and two children both having value 2 (since 2 × 2 = 4)
The solution uses dynamic programming where f[i]
represents the number of binary trees that can be formed with arr[i]
as the root. For each element arr[i]
, it checks all possible factorizations where arr[i] = arr[j] × arr[k]
and accumulates the count by multiplying the number of trees possible with arr[j]
as root and arr[k]
as root.
Intuition
The key insight is recognizing this as a dynamic programming problem where we build up solutions for larger numbers using solutions for smaller numbers.
Think about it this way: if we want to count trees with a particular value a
as the root, we need to consider all possible ways to factor a
into two parts that will become its children. For instance, if a = 12
and we have 3
and 4
in our array, then 12 = 3 × 4
, so we can make a tree with root 12
and children 3
and 4
.
But here's the crucial observation: the number of trees with root 12
depends on how many different trees we can make with 3
as root and how many with 4
as root. If there are x
ways to make trees with root 3
and y
ways to make trees with root 4
, then there are x × y
ways to make trees with root 12
having these specific children.
This naturally leads to a bottom-up approach:
- Start with the smallest numbers - they can only form single-node trees (count = 1)
- For larger numbers, check all possible factorizations
- For each valid factorization
a = b × c
, multiply the counts of trees rooted atb
andc
Why sort the array? By processing numbers in ascending order, we ensure that when we're calculating the count for a number a
, we've already computed the counts for all its potential factors (which must be smaller than a
).
The formula f[i] = sum(f[j] × f[k])
for all valid pairs (j, k)
where arr[i] = arr[j] × arr[k]
captures this multiplicative counting principle. Each valid factorization contributes independently to the total count, which is why we add them together.
Learn more about Dynamic Programming and Sorting patterns.
Solution Approach
The solution implements a dynamic programming approach with the following steps:
1. Sorting and Indexing
arr.sort()
idx = {v: i for i, v in enumerate(arr)}
First, we sort the array in ascending order. This ensures we process smaller numbers before larger ones. We also create a dictionary idx
that maps each value to its index position for O(1) lookup time when checking if a factor exists in our array.
2. Initialize DP Array
f = [1] * n
We initialize a DP array f
where f[i]
represents the number of binary trees that can be formed with arr[i]
as the root. Each element starts with value 1 because every number can form at least one tree (a single node).
3. Core DP Logic
for i, a in enumerate(arr):
for j in range(i):
b = arr[j]
if a % b == 0 and (c := (a // b)) in idx:
f[i] = (f[i] + f[j] * f[idx[c]]) % mod
For each number a
at position i
:
- We iterate through all smaller numbers
b
at positionsj < i
- Check if
b
is a factor ofa
usinga % b == 0
- If yes, calculate the complementary factor
c = a // b
- Check if
c
exists in our array using theidx
dictionary - If both factors exist, we can form trees with
a
as root,b
as left child, andc
as right child - The number of such trees is
f[j] × f[idx[c]]
(multiplication principle) - Add this to our running total for
f[i]
4. Modular Arithmetic
Throughout the computation, we apply modulo 10^9 + 7
to prevent integer overflow, as the counts can become very large.
5. Final Result
return sum(f) % mod
The total number of binary trees is the sum of all f[i]
values, as each element can serve as the root of different trees.
Time Complexity: O(n²) where n is the length of the array, as we have nested loops over the array elements.
Space Complexity: O(n) for the DP array and the index dictionary.
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 the solution with arr = [2, 4, 8]
.
Step 1: Sort and Index
- Sorted array:
[2, 4, 8]
(already sorted) - Index mapping:
{2: 0, 4: 1, 8: 2}
- Initialize DP:
f = [1, 1, 1]
(each number can form a single-node tree)
Step 2: Process arr[0] = 2
i = 0
,a = 2
- No previous elements to check (j < 0)
f[0]
remains 1 (only single-node tree possible)
Step 3: Process arr[1] = 4
i = 1
,a = 4
- Check
j = 0
:b = 2
- Is 2 a factor of 4? Yes (4 % 2 = 0)
- Complementary factor:
c = 4 // 2 = 2
- Is 2 in our array? Yes (at index 0)
- Add trees:
f[1] = 1 + f[0] × f[0] = 1 + 1 × 1 = 2
f[1] = 2
(one single-node tree + one tree with root 4 and children 2,2)
Step 4: Process arr[2] = 8
i = 2
,a = 8
- Check
j = 0
:b = 2
- Is 2 a factor of 8? Yes
- Complementary factor:
c = 8 // 2 = 4
- Is 4 in our array? Yes (at index 1)
- Add trees:
f[2] = 1 + f[0] × f[1] = 1 + 1 × 2 = 3
- Check
j = 1
:b = 4
- Is 4 a factor of 8? Yes
- Complementary factor:
c = 8 // 4 = 2
- Is 2 in our array? Yes (at index 0)
- Add trees:
f[2] = 3 + f[1] × f[0] = 3 + 2 × 1 = 5
f[2] = 5
Step 5: Calculate Total
- Total =
f[0] + f[1] + f[2] = 1 + 2 + 5 = 8
The 8 possible trees are:
- Single node: 2
- Single node: 4
- Tree: 4 (root) with children 2, 2
- Single node: 8
- Tree: 8 (root) with children 2, 4
- Tree: 8 (root) with children 4, 2
- Tree: 8 (root) with children 2, 4 (where 4 itself has children 2, 2)
- Tree: 8 (root) with children 4, 2 (where 4 itself has children 2, 2)
Solution Implementation
1class Solution:
2 def numFactoredBinaryTrees(self, arr: List[int]) -> int:
3 MOD = 10**9 + 7
4 n = len(arr)
5
6 # Sort array to ensure we process smaller values first
7 arr.sort()
8
9 # Create a mapping from value to its index for O(1) lookup
10 value_to_index = {value: index for index, value in enumerate(arr)}
11
12 # dp[i] represents the number of binary trees that can be formed with arr[i] as root
13 dp = [1] * n # Each number can form at least one tree (itself as a single node)
14
15 # Process each element as a potential root
16 for i, root_value in enumerate(arr):
17 # Try all smaller elements as potential left child
18 for j in range(i):
19 left_child = arr[j]
20
21 # Check if root_value is divisible by left_child
22 if root_value % left_child == 0:
23 right_child = root_value // left_child
24
25 # Check if the right child exists in our array
26 if right_child in value_to_index:
27 right_child_index = value_to_index[right_child]
28 # Add the number of trees formed by combining left and right subtrees
29 dp[i] = (dp[i] + dp[j] * dp[right_child_index]) % MOD
30
31 # Return the total number of binary trees that can be formed
32 return sum(dp) % MOD
33
1class Solution {
2 public int numFactoredBinaryTrees(int[] arr) {
3 // Define modulo constant for large number handling
4 final int MOD = 1_000_000_007;
5
6 // Sort the array to process smaller values first
7 Arrays.sort(arr);
8
9 int n = arr.length;
10
11 // dp[i] represents the number of binary trees that can be formed with arr[i] as root
12 long[] dp = new long[n];
13 Arrays.fill(dp, 1); // Each element can form at least one tree (itself as a single node)
14
15 // Create a map to quickly find the index of a value in the sorted array
16 Map<Integer, Integer> valueToIndex = new HashMap<>(n);
17 for (int i = 0; i < n; ++i) {
18 valueToIndex.put(arr[i], i);
19 }
20
21 // Process each element as a potential root
22 for (int i = 0; i < n; ++i) {
23 int root = arr[i];
24
25 // Try all smaller elements as potential left child
26 for (int j = 0; j < i; ++j) {
27 int leftChild = arr[j];
28
29 // Check if root is divisible by leftChild
30 if (root % leftChild == 0) {
31 int rightChild = root / leftChild;
32
33 // Check if the corresponding right child exists in the array
34 if (valueToIndex.containsKey(rightChild)) {
35 int rightChildIndex = valueToIndex.get(rightChild);
36
37 // Add the number of trees formed by combining left and right subtrees
38 // dp[j] * dp[rightChildIndex] gives all combinations of left and right subtrees
39 dp[i] = (dp[i] + dp[j] * dp[rightChildIndex]) % MOD;
40 }
41 }
42 }
43 }
44
45 // Calculate the total number of binary trees by summing all possibilities
46 long totalTrees = 0;
47 for (long treeCount : dp) {
48 totalTrees = (totalTrees + treeCount) % MOD;
49 }
50
51 return (int) totalTrees;
52 }
53}
54
1class Solution {
2public:
3 int numFactoredBinaryTrees(vector<int>& arr) {
4 const int MOD = 1e9 + 7;
5
6 // Sort array to process smaller values first
7 sort(arr.begin(), arr.end());
8
9 // Create a map to store value to index mapping for O(1) lookup
10 unordered_map<int, int> valueToIndex;
11 int n = arr.size();
12 for (int i = 0; i < n; ++i) {
13 valueToIndex[arr[i]] = i;
14 }
15
16 // dp[i] represents the number of binary trees that can be formed with arr[i] as root
17 vector<long> dp(n, 1); // Initialize with 1 (single node tree)
18
19 // For each potential root value
20 for (int i = 0; i < n; ++i) {
21 int rootValue = arr[i];
22
23 // Try all possible left child values (smaller than root)
24 for (int j = 0; j < i; ++j) {
25 int leftChildValue = arr[j];
26
27 // Check if rootValue is divisible by leftChildValue
28 if (rootValue % leftChildValue == 0) {
29 int rightChildValue = rootValue / leftChildValue;
30
31 // Check if rightChildValue exists in our array
32 if (valueToIndex.count(rightChildValue)) {
33 int rightChildIndex = valueToIndex[rightChildValue];
34 // Add the product of possible left and right subtrees
35 dp[i] = (dp[i] + 1L * dp[j] * dp[rightChildIndex]) % MOD;
36 }
37 }
38 }
39 }
40
41 // Sum up all possible trees with different root values
42 long totalTrees = 0;
43 for (long treeCount : dp) {
44 totalTrees = (totalTrees + treeCount) % MOD;
45 }
46
47 return totalTrees;
48 }
49};
50
1/**
2 * Counts the number of binary trees that can be formed using array elements
3 * where each non-leaf node's value is the product of its children's values
4 * @param arr - Array of unique positive integers
5 * @returns Number of possible binary trees modulo 10^9 + 7
6 */
7function numFactoredBinaryTrees(arr: number[]): number {
8 const MOD = 10 ** 9 + 7;
9
10 // Sort array in ascending order to ensure we process smaller values first
11 arr.sort((a, b) => a - b);
12
13 // Create a map to store the index of each element for O(1) lookup
14 const valueToIndex: Map<number, number> = new Map();
15 const arrayLength = arr.length;
16
17 // Populate the index map
18 for (let i = 0; i < arrayLength; ++i) {
19 valueToIndex.set(arr[i], i);
20 }
21
22 // dp[i] represents the number of binary trees that can be formed with arr[i] as root
23 const dp: number[] = new Array(arrayLength).fill(1);
24
25 // Process each element as a potential root
26 for (let i = 0; i < arrayLength; ++i) {
27 const currentValue = arr[i];
28
29 // Try all smaller elements as potential left child
30 for (let j = 0; j < i; ++j) {
31 const leftChildValue = arr[j];
32
33 // Check if currentValue is divisible by leftChildValue
34 if (currentValue % leftChildValue === 0) {
35 const rightChildValue = currentValue / leftChildValue;
36
37 // Check if the right child value exists in our array
38 if (valueToIndex.has(rightChildValue)) {
39 const rightChildIndex = valueToIndex.get(rightChildValue)!;
40
41 // Add the number of trees formed by combining left and right subtrees
42 // dp[j] * dp[rightChildIndex] gives all combinations of left and right subtrees
43 dp[i] = (dp[i] + dp[j] * dp[rightChildIndex]) % MOD;
44 }
45 }
46 }
47 }
48
49 // Sum up all possible trees and return the result modulo MOD
50 return dp.reduce((sum, count) => sum + count) % MOD;
51}
52
Time and Space Complexity
Time Complexity: O(n²)
where n
is the length of the input array.
- Sorting the array takes
O(n log n)
time - Creating the index dictionary takes
O(n)
time - The nested loops iterate through all pairs where
i > j
, which gives us approximatelyn²/2
iterations - For each iteration, we perform:
- A modulo operation:
O(1)
- A division operation:
O(1)
- A dictionary lookup to check if
c
exists:O(1)
- If the condition is met, another dictionary lookup and arithmetic operations:
O(1)
- A modulo operation:
- The final sum operation takes
O(n)
time
The dominant operation is the nested loops with O(n²)
iterations, making the overall time complexity O(n²)
.
Space Complexity: O(n)
where n
is the length of the input array.
- The sorted array (in-place sorting in Python creates a new list):
O(n)
- The index dictionary
idx
storesn
key-value pairs:O(n)
- The dynamic programming array
f
storesn
values:O(n)
- Temporary variables like
i
,j
,a
,b
,c
:O(1)
The total space complexity is O(n)
as we store linear amounts of data relative to the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle Duplicate Factor Pairs
A common mistake is counting the same tree structure twice when both factors can serve as either left or right child. For instance, if we have 12 = 3 × 4
and 12 = 4 × 3
, these represent different tree structures (3 as left child vs. 4 as left child), and both should be counted.
Why this isn't actually a problem in the given solution: The algorithm naturally handles this by iterating through all possible left children. When j
corresponds to value 3, it finds right child 4. When j
corresponds to value 4, it finds right child 3. Both configurations are correctly counted.
2. Integer Overflow Issues
The most critical pitfall is forgetting to apply modulo operations at every multiplication step. Without proper modulo application, the intermediate results can overflow even with 64-bit integers.
Incorrect approach:
# WRONG - can cause overflow
dp[i] = dp[i] + dp[j] * dp[right_child_index]
result = sum(dp) % MOD # Too late to apply modulo
Correct approach:
# RIGHT - apply modulo after each operation
dp[i] = (dp[i] + dp[j] * dp[right_child_index]) % MOD
result = sum(dp) % MOD
3. Not Sorting the Array First
A critical mistake is processing the array without sorting it first. The DP solution relies on the fact that when computing dp[i]
, all potential children values (which are smaller) have already been processed.
Why sorting is essential:
- Without sorting, you might try to use
dp[j]
before it's been properly computed - The algorithm assumes that for any root value, its factors (potential children) have been processed first
- Since factors are always smaller than or equal to their product, sorting ensures correct processing order
Example of the problem:
If arr = [4, 2, 8]
and we don't sort:
- When processing 4 (index 0), we might miss that 2 (index 1) could be its child
- When processing 8 (index 2), the value for dp[0] (for 4) might not be fully computed
4. Inefficient Factor Checking
Another pitfall is checking if the complementary factor exists using a linear search instead of using a hash map/dictionary for O(1) lookup.
Inefficient approach:
# WRONG - O(n) search for each check if right_child in arr: # Linear search right_child_index = arr.index(right_child) # Another linear search
Efficient approach:
# RIGHT - O(1) lookup using dictionary if right_child in value_to_index: # O(1) check right_child_index = value_to_index[right_child] # O(1) access
5. Missing Edge Cases
Forgetting that each number can form a single-node tree is a subtle but important oversight. The initialization dp = [1] * n
handles this, but it's easy to mistakenly initialize with zeros.
Wrong initialization:
dp = [0] * n # WRONG - misses single-node trees
Correct initialization:
dp = [1] * n # RIGHT - each number forms at least one tree (itself)
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!