Binary Trees With Factors
MedPrompt
Given an array of unique integers, arr, where each integer arr[i] is strictly greater than 1.
We make a binary tree using these integers, and each number may be used for any number of times. Each non-leaf node's value should be equal to the product of the values of its children.
Return the number of binary trees we can make. The answer may be too large so return the answer modulo 109 + 7.
Example 1:
[2], [4], [4, 2, 2]Example 2:
[2], [4], [5], [10], [4, 2, 2], [10, 2, 5], [10, 5, 2]
Constraints:
1 <= arr.length <= 10002 <= arr[i] <= 109- All the values of
arrare unique.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a plain recursive function to count the number of trees for each number in the input array. For each number x, we consider it as a root. The number of trees with root x is 1 (for the leaf node) plus the sum of products of the number of trees for its possible children. We find pairs of factors (y, z) of x that are also present in the input array. The number of ways to form a tree with x as the root and y, z as children is the product of the number of ways to form trees with root y and root z. This method does not use memoization, leading to re-computation of results for the same subproblems.
Algorithm
- Create a
HashSetfrom the inputarrfor O(1) average time lookups. - Initialize
totalTrees = 0. - For each
numinarr: a. Add the result of a recursive functioncountTrees(num)tototalTrees(modulo10^9 + 7). - Return
totalTrees.
countTrees(root) function:
- Initialize
count = 1(for the leaf node). - For each
factor1inarr: a. Ifrootis divisible byfactor1: i. Letfactor2 = root / factor1. ii. Iffactor2is in theHashSet: - Recursively callcountTrees(factor1)andcountTrees(factor2). - Add the product of their results tocount(modulo10^9 + 7). - Return
count.
Walkthrough
We define a recursive function, say countTrees(root), which calculates the number of possible binary trees with the given root value. To make checking for the existence of factors efficient, we first put all elements of arr into a HashSet.
The main function iterates through each number in arr and calls countTrees for it, summing up the results modulo 10^9 + 7.
Inside countTrees(root):
- Initialize a counter
countto 1, representing the tree with only therootnode. - Iterate through each number
factor1in the input arrayarr. - If
rootis divisible byfactor1, calculatefactor2 = root / factor1. - Check if
factor2also exists in theHashSetof array elements. - If it exists, it means we found a valid pair of children. We recursively call
countTrees(factor1)andcountTrees(factor2)and add their product to ourcount. - Return the final
count.
This approach is very slow because it recomputes the results for the same subproblems multiple times. For example, countTrees(2) would be calculated repeatedly whenever 2 appears as a factor in the recursion tree.
// Note: This solution is for demonstration and will cause Time Limit Exceeded.import java.util.HashSet;import java.util.Set; class Solution { private Set<Integer> numSet; private int[] arr; private long MOD = 1_000_000_007; public int numFactoredBinaryTrees(int[] arr) { this.arr = arr; this.numSet = new HashSet<>(); for (int x : arr) { numSet.add(x); } long totalTrees = 0; for (int num : arr) { totalTrees = (totalTrees + countTrees(num)) % MOD; } return (int) totalTrees; } private long countTrees(int root) { long count = 1; // The tree with just the root for (int factor1 : arr) { if (root > factor1 && root % factor1 == 0) { int factor2 = root / factor1; if (numSet.contains(factor2)) { long ways1 = countTrees(factor1); long ways2 = countTrees(factor2); count = (count + (ways1 * ways2)) % MOD; } } } return count; }}Complexity
Time
Exponential, likely O(N^N) or worse. The number of recursive calls grows very rapidly without memoization, as each call can spawn `O(N)` new pairs of recursive calls.
Space
O(N), where N is the length of `arr`. This is for the recursion stack depth in the worst case (e.g., for an input like `[2, 4, 8, 16, ...]`).
Trade-offs
Pros
Simple to conceptualize as it directly translates the problem's recursive structure.
Cons
Extremely inefficient due to massive redundant computations.
Will result in a 'Time Limit Exceeded' (TLE) error for all but the smallest inputs.
The recursion depth can be large, potentially leading to a
StackOverflowError.
This is an efficient approach that avoids recomputing results by storing them using dynamic programming. The core idea is that the number of trees with root x can be calculated if we already know the number of trees for all of x's factors that are present in the input array. By sorting the input array, we guarantee that when we process a number, we have already computed and stored the results for all its smaller factors.
Algorithm
- Define
MOD = 10^9 + 7. - Sort the input array
arrin ascending order. - Create a
HashMap<Integer, Long> dpto store the number of trees for each root value. - Initialize
totalTrees = 0. - For
ifrom0toarr.length - 1: a. Letnum = arr[i]. b. Initialize a localcount = 1L(for the tree with a single nodenum). c. Forjfrom0toi - 1: i. Letfactor1 = arr[j]. ii. Ifnum % factor1 == 0: - Letfactor2 = num / factor1. - Ifdpcontainsfactor2as a key: -count = (count + dp.get(factor1) * dp.get(factor2)) % MOD. d. Put the finalcountinto the map:dp.put(num, count). e. AddcounttototalTrees:totalTrees = (totalTrees + count) % MOD. - Return
totalTreesas an integer.
Walkthrough
First, we sort the input array arr in ascending order. This ensures that for any number arr[i], any of its factors that are also in arr must appear before it (i.e., at an index j < i).
We use a HashMap, let's call it dp, to store the number of trees that can be formed with each number as the root. The key is the number from arr, and the value is the count of trees.
We iterate through the sorted array. For each number num = arr[i]:
- We initialize a
countfornumto 1. This represents the base case: a tree consisting of only the leaf nodenum. - We then search for pairs of factors. We iterate through the previously processed numbers
factor1 = arr[j](wherej < i). - If
numis divisible byfactor1, we find the other factor,factor2 = num / factor1. - We check if
factor2also exists in ourdpmap. Since the array is sorted, iffactor2exists inarr, it must have been processed already and will be indp. - If
factor2is indp, we've found a valid pair of children. The number of new trees we can form isdp.get(factor1) * dp.get(factor2). We add this product to ourcount. - After checking all possible
factor1s, we store the finalcountin our map:dp.put(num, count).
The total number of trees is the sum of all values in the dp map. We keep a running total, updated in each step of the outer loop, to avoid a final summation loop. All additions and multiplications are performed modulo 10^9 + 7.
import java.util.Arrays;import java.util.HashMap;import java.util.Map; class Solution { public int numFactoredBinaryTrees(int[] arr) { long MOD = 1_000_000_007; int n = arr.length; Arrays.sort(arr); Map<Integer, Long> dp = new HashMap<>(); long totalTrees = 0; for (int i = 0; i < n; i++) { int root = arr[i]; long count = 1L; // Base case: the node itself is a tree for (int j = 0; j < i; j++) { int factor1 = arr[j]; if (root % factor1 == 0) { int factor2 = root / factor1; if (dp.containsKey(factor2)) { long ways1 = dp.get(factor1); long ways2 = dp.get(factor2); count = (count + (ways1 * ways2)) % MOD; } } } dp.put(root, count); totalTrees = (totalTrees + count) % MOD; } return (int) totalTrees; }}Complexity
Time
O(N^2), where N is the length of `arr`. Sorting the array takes `O(N log N)`. The dominant part is the nested loop structure. The outer loop runs N times, and the inner loop runs up to N times in the worst case.
Space
O(N), where N is the length of `arr`. This space is used to store the `dp` map which holds an entry for each number in the input array.
Trade-offs
Pros
Highly efficient and guaranteed to pass within the time limits.
Avoids recursion overhead and potential stack overflow issues.
The logic is clear and builds the solution from smaller subproblems to larger ones.
Cons
The
O(N^2)time complexity might be a concern for much larger constraints, but it is efficient enough for the given problem size.
Solutions
Solution
class Solution { public int numFactoredBinaryTrees ( int [] arr ) { final int mod = ( int ) 1 e9 + 7 ; Arrays . sort ( arr ); int n = arr . length ; long [] f = new long [ n ]; Arrays . fill ( f , 1 ); Map < Integer , Integer > idx = new HashMap <>( n ); for ( int i = 0 ; i < n ; ++ i ) { idx . put ( arr [ i ], i ); } for ( int i = 0 ; i < n ; ++ i ) { int a = arr [ i ]; for ( int j = 0 ; j < i ; ++ j ) { int b = arr [ j ]; if ( a % b == 0 ) { int c = a / b ; if ( idx . containsKey ( c )) { int k = idx . get ( c ); f [ i ] = ( f [ i ] + f [ j ] * f [ k ]) % mod ; } } } } long ans = 0 ; for ( long v : f ) { ans = ( ans + v ) % mod ; } return ( int ) ans ; } }class Solution: def numFactoredBinaryTrees(self, arr: List[int]) -> int: mod = 10 ** 9 + 7 n = len(arr) arr . sort() idx = {v: i for i, v in enumerate(arr)} f = [1] * n 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 return sum(f) % modclass Solution {public: int numFactoredBinaryTrees(vector<int> &arr) { const int mod = 1e9 + 7; sort(arr.begin(), arr.end()); unordered_map<int, int> idx; int n = arr.size(); for (int i = 0; i < n; ++i) { idx[arr[i]] = i; } vector<long> f(n, 1); for (int i = 0; i < n; ++i) { int a = arr[i]; for (int j = 0; j < i; ++j) { int b = arr[j]; if (a % b == 0) { int c = a / b; if (idx.count(c)) { int k = idx[c]; f[i] = (f[i] + 1l * f[j] * f[k]) % mod; } } } } long ans = 0; for (long v : f) { ans = (ans + v) % mod; } return ans; }};Video 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.