254. Factor Combinations 🔒
Problem Description
This problem asks you to find all possible ways to express a given integer n
as a product of its factors.
Given an integer n
, you need to return all possible combinations where n
can be written as a product of two or more factors. Each factor must be in the range [2, n-1]
, meaning factors cannot be 1 or n
itself.
For example, if n = 8
:
8
can be expressed as2 × 2 × 2
8
can also be expressed as2 × 4
Both of these factorizations would be valid answers.
The key requirements are:
- Each combination should multiply together to equal
n
- All factors in a combination must be at least 2 and at most
n-1
- You need to find ALL possible combinations, not just one
- The order of combinations in your output doesn't matter
- Each combination itself should be a list of factors
The solution uses a depth-first search (DFS) approach with backtracking. It starts with factor 2 and tries to recursively decompose n
by dividing it with valid factors. The variable t
maintains the current combination being built, and when a valid factorization is found (when t
is non-empty), it's added to the answer along with the remaining value of n
. The algorithm ensures no duplicate combinations by only considering factors greater than or equal to the last used factor (tracked by parameter i
).
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem doesn't involve nodes and edges or graph traversal. We're finding factor combinations of a number, not exploring a graph structure.
Need to solve for kth smallest/largest?
- No: We need to find ALL possible factorizations, not just the kth smallest or largest one.
Involves Linked Lists?
- No: The problem deals with integer factorization, not linked list operations.
Does the problem have small constraints?
- Yes: While not explicitly stated, factorization problems typically have manageable constraints. The number of valid factorizations for a given
n
is limited, and we need to explore all possible combinations systematically. The constraint that factors must be in range[2, n-1]
further limits the search space.
Brute force / Backtracking?
- Yes: This is a classic backtracking problem. We need to:
- Try each valid factor starting from 2
- Recursively decompose the remaining number
- Backtrack when we've explored all possibilities with a given factor
- Build all possible combinations by exploring different factorization paths
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This problem requires us to explore all possible ways to decompose a number into factors, making decisions at each step (which factor to use next), and backtracking to try different combinations - the hallmark characteristics of backtracking problems.
Intuition
Think about how you would manually find all factorizations of a number like 12. You'd start with the smallest factor (2) and ask: "Can 12 be divided by 2?" Yes, it gives us 6. Now you have a partial factorization: [2, ?]
. But what about the remaining 6? You need to recursively apply the same process - can 6 be further factored? This gives you the idea of breaking down the problem recursively.
The key insight is that factorization is inherently a decision tree problem. At each step, you decide: "Which factor should I try next?" For example, with 12:
- Try factor 2:
12 = 2 × 6
, then recursively factor 6 - Try factor 3:
12 = 3 × 4
, then recursively factor 4 - Try factor 4:
12 = 4 × 3
, but wait - this would duplicate[3, 4]
This brings us to an important observation: to avoid duplicates, we should only consider factors in non-decreasing order. If we've used factor i
, the next factor should be at least i
. This ensures we get [2, 2, 3]
but not [2, 3, 2]
or [3, 2, 2]
.
The backtracking pattern emerges naturally: we build a combination by choosing factors one by one, explore all possibilities with that choice, then "undo" the choice (backtrack) to try different factors. When we can't factor anymore (or choose not to), we've found a complete factorization.
The stopping condition is elegant: whenever we have at least one factor in our current path (meaning t
is non-empty), we can stop and record the current combination by appending the remaining number. This remaining number itself becomes the last factor. For instance, if we have [2]
and the remaining number is 6, we get the factorization [2, 6]
.
Solution Approach
The solution implements a depth-first search with backtracking using a helper function dfs(n, i)
where n
is the current number to factor and i
is the minimum factor we can use (to avoid duplicates).
Core Data Structures:
t
: A temporary list that acts as our current path, storing factors we're building upans
: The result list that collects all valid factorizations
Algorithm Walkthrough:
-
Recording Valid Factorizations: At the start of each
dfs
call, ift
is non-empty (meaning we have at least one factor), we record a valid factorization by appendingt + [n]
to our answer. This captures factorizations wheren
is the last factor. -
Exploring Factors: We iterate through potential factors starting from
i
(initially 2). The loop continues whilej * j <= n
because:- If
j > sqrt(n)
, thenn/j < sqrt(n)
, meaning we've already considered this factorization from the other side - This optimization prevents duplicate work and ensures termination
- If
-
Recursive Decomposition: When we find a valid factor
j
(wheren % j == 0
):- Add
j
to our current patht
- Recursively call
dfs(n // j, j)
to factor the quotient - The key is passing
j
as the new minimum factor - this ensures all subsequent factors are>= j
, maintaining non-decreasing order - After exploring all possibilities with this factor, backtrack by removing
j
fromt
- Add
-
Backtracking Pattern: The
t.append(j)
followed byt.pop()
implements the classic backtracking pattern:- Make a choice (add factor
j
) - Explore recursively with that choice
- Undo the choice to try other possibilities
- Make a choice (add factor
Example Trace for n=12:
- Start with
dfs(12, 2)
,t = []
- Try factor 2:
t = [2]
, calldfs(6, 2)
- In
dfs(6, 2)
: record[2, 6]
- Try factor 2:
t = [2, 2]
, calldfs(3, 2)
- In
dfs(3, 2)
: record[2, 2, 3]
- In
- Try factor 3:
t = [2, 3]
, calldfs(2, 3)
- In
dfs(2, 3)
: record[2, 3, 2]
→ Actually this won't happen since3 * 3 > 2
- In
- In
- Try factor 3:
t = [3]
, calldfs(4, 3)
- In
dfs(4, 3)
: record[3, 4]
- In
The algorithm guarantees we find all unique factorizations without duplicates through the non-decreasing factor constraint.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through finding all factorizations of n = 12
.
Initial Call: dfs(12, 2)
with t = []
Step 1: First level - trying factor 2
- Since
t
is empty, we don't record anything yet - Check factors starting from 2:
2 * 2 = 4 ≤ 12
✓ 12 % 2 = 0
, so 2 is a valid factor- Add 2 to path:
t = [2]
- Recursively call
dfs(6, 2)
Step 2: Inside dfs(6, 2)
with t = [2]
- Since
t
is non-empty, record[2] + [6] = [2, 6]
✓ - Check factors starting from 2:
2 * 2 = 4 ≤ 6
✓ 6 % 2 = 0
, so 2 is valid- Add 2 to path:
t = [2, 2]
- Recursively call
dfs(3, 2)
Step 3: Inside dfs(3, 2)
with t = [2, 2]
- Since
t
is non-empty, record[2, 2] + [3] = [2, 2, 3]
✓ - Check factors starting from 2:
2 * 2 = 4 > 3
✗ - No valid factors, return
Step 4: Back in dfs(6, 2)
, try next factor
- Remove 2 from path (backtrack):
t = [2]
- Try factor 3:
3 * 3 = 9 > 6
✗ - No more factors, return
Step 5: Back in dfs(12, 2)
, try next factor
- Remove 2 from path (backtrack):
t = []
- Try factor 3:
3 * 3 = 9 ≤ 12
✓ 12 % 3 = 0
, so 3 is valid- Add 3 to path:
t = [3]
- Recursively call
dfs(4, 3)
Step 6: Inside dfs(4, 3)
with t = [3]
- Since
t
is non-empty, record[3] + [4] = [3, 4]
✓ - Check factors starting from 3:
3 * 3 = 9 > 4
✗ - No valid factors, return
Step 7: Final cleanup
- Back in
dfs(12, 2)
, remove 3 from path:t = []
- Try factor 4:
4 * 4 = 16 > 12
✗ - No more factors to try
Final Result: [[2, 6], [2, 2, 3], [3, 4]]
The key insights from this walkthrough:
- We only try factors where
j * j ≤ n
to avoid redundant work - By maintaining
i
as the minimum factor, we ensure factors are in non-decreasing order (preventing duplicates like[2, 3, 2]
) - Backtracking allows us to explore all possible factor combinations systematically
- We record a factorization whenever we have at least one factor in our path, using the remaining value as the last factor
Solution Implementation
1class Solution:
2 def getFactors(self, n: int) -> List[List[int]]:
3 """
4 Find all possible combinations of factors that multiply to n.
5 Factors must be greater than 1 and less than n.
6
7 Args:
8 n: The target number to factorize
9
10 Returns:
11 List of lists containing all possible factor combinations
12 """
13
14 def dfs(target: int, start_factor: int) -> None:
15 """
16 Depth-first search to find factor combinations.
17
18 Args:
19 target: Current number to factorize
20 start_factor: Minimum factor to consider (avoids duplicates)
21 """
22 # If we have factors in current_path, add this combination
23 # (current factors + remaining target as last factor)
24 if current_path:
25 result.append(current_path + [target])
26
27 # Try all possible factors from start_factor up to sqrt(target)
28 factor = start_factor
29 while factor * factor <= target:
30 # If factor divides target evenly
31 if target % factor == 0:
32 # Add factor to current path
33 current_path.append(factor)
34 # Recursively find factors for the quotient
35 # Use 'factor' as start to avoid duplicate combinations
36 dfs(target // factor, factor)
37 # Backtrack - remove the factor we just tried
38 current_path.pop()
39 factor += 1
40
41 # Initialize tracking variables
42 current_path = [] # Stores current combination of factors being explored
43 result = [] # Stores all valid factor combinations
44
45 # Start DFS with the original number and minimum factor of 2
46 dfs(n, 2)
47
48 return result
49
1class Solution {
2 // List to store the current combination of factors being explored
3 private List<Integer> currentFactors = new ArrayList<>();
4 // List to store all valid factor combinations
5 private List<List<Integer>> allFactorCombinations = new ArrayList<>();
6
7 /**
8 * Finds all possible combinations of factors for a given number n
9 * (excluding 1 and n itself as the only factors)
10 *
11 * @param n The number to factorize
12 * @return List of all valid factor combinations
13 */
14 public List<List<Integer>> getFactors(int n) {
15 // Start DFS with the smallest possible factor (2)
16 findFactorCombinations(n, 2);
17 return allFactorCombinations;
18 }
19
20 /**
21 * Recursive DFS method to find all factor combinations
22 *
23 * @param remainingNumber The current number to be factorized
24 * @param startFactor The minimum factor to consider (ensures non-decreasing order)
25 */
26 private void findFactorCombinations(int remainingNumber, int startFactor) {
27 // If we have factors in our current path, we can form a valid combination
28 // by adding the remaining number as the last factor
29 if (!currentFactors.isEmpty()) {
30 List<Integer> validCombination = new ArrayList<>(currentFactors);
31 validCombination.add(remainingNumber);
32 allFactorCombinations.add(validCombination);
33 }
34
35 // Try all possible factors from startFactor up to sqrt(remainingNumber)
36 // Using factor <= remainingNumber / factor to avoid overflow and ensure factor * factor <= remainingNumber
37 for (int factor = startFactor; factor <= remainingNumber / factor; factor++) {
38 // Check if factor divides remainingNumber evenly
39 if (remainingNumber % factor == 0) {
40 // Add current factor to the path
41 currentFactors.add(factor);
42 // Recursively find factors for the quotient
43 // Use 'factor' as the new startFactor to maintain non-decreasing order
44 findFactorCombinations(remainingNumber / factor, factor);
45 // Backtrack: remove the factor to try other combinations
46 currentFactors.remove(currentFactors.size() - 1);
47 }
48 }
49 }
50}
51
1class Solution {
2public:
3 vector<vector<int>> getFactors(int n) {
4 vector<int> currentFactors; // Store current combination of factors
5 vector<vector<int>> result; // Store all valid factor combinations
6
7 // Lambda function for DFS traversal to find all factor combinations
8 function<void(int, int)> dfs = [&](int remainingNumber, int startFactor) {
9 // If we have at least one factor in current path, we can form a valid combination
10 // by adding the remaining number as the last factor
11 if (!currentFactors.empty()) {
12 vector<int> combination = currentFactors;
13 combination.push_back(remainingNumber);
14 result.push_back(combination);
15 }
16
17 // Try all possible factors starting from startFactor
18 // Loop condition: factor * factor <= remainingNumber ensures we don't duplicate combinations
19 for (int factor = startFactor; factor * factor <= remainingNumber; ++factor) {
20 // Check if factor divides remainingNumber evenly
21 if (remainingNumber % factor == 0) {
22 // Include this factor in current path
23 currentFactors.push_back(factor);
24
25 // Recursively find factors for the quotient
26 // Pass factor as new startFactor to avoid duplicates (maintain non-decreasing order)
27 dfs(remainingNumber / factor, factor);
28
29 // Backtrack: remove the factor from current path
30 currentFactors.pop_back();
31 }
32 }
33 };
34
35 // Start DFS with smallest possible factor 2
36 dfs(n, 2);
37
38 return result;
39 }
40};
41
1function getFactors(n: number): number[][] {
2 const currentFactors: number[] = []; // Store current combination of factors
3 const result: number[][] = []; // Store all valid factor combinations
4
5 // DFS traversal function to find all factor combinations
6 const dfs = (remainingNumber: number, startFactor: number): void => {
7 // If we have at least one factor in current path, we can form a valid combination
8 // by adding the remaining number as the last factor
9 if (currentFactors.length > 0) {
10 const combination = [...currentFactors, remainingNumber];
11 result.push(combination);
12 }
13
14 // Try all possible factors starting from startFactor
15 // Loop condition: factor * factor <= remainingNumber ensures we don't duplicate combinations
16 for (let factor = startFactor; factor * factor <= remainingNumber; factor++) {
17 // Check if factor divides remainingNumber evenly
18 if (remainingNumber % factor === 0) {
19 // Include this factor in current path
20 currentFactors.push(factor);
21
22 // Recursively find factors for the quotient
23 // Pass factor as new startFactor to avoid duplicates (maintain non-decreasing order)
24 dfs(Math.floor(remainingNumber / factor), factor);
25
26 // Backtrack: remove the factor from current path
27 currentFactors.pop();
28 }
29 }
30 };
31
32 // Start DFS with smallest possible factor 2
33 dfs(n, 2);
34
35 return result;
36}
37
Time and Space Complexity
Time Complexity: O(n^(log n))
The time complexity analysis is quite complex for this problem. In the worst case, we're exploring all possible factorizations of n
. For each valid factor j
where j * j <= n
, we recursively call dfs(n // j, j)
. The number of recursive calls depends on the factorization tree of n
.
- The branching factor at each level can be up to
O(sqrt(n))
(all factors fromi
tosqrt(n)
) - The depth of recursion is at most
O(log n)
(when we repeatedly divide by 2) - However, the actual number of valid factorizations is bounded by the partition function, which grows exponentially but is much less than
n^(log n)
- A tighter bound would be
O(2^(sqrt(n)))
for the number of factorizations, but considering the work done at each node, the overall complexity is approximatelyO(n^(log n))
Space Complexity: O(log n)
The space complexity consists of:
- Recursion stack depth:
O(log n)
- The maximum depth occurs when we continuously divide by the smallest factor (2), giving us at mostlogâ‚‚(n)
levels - The temporary list
t
:O(log n)
- At most containslogâ‚‚(n)
factors at any point - The answer list
ans
: This stores all valid factorizations, which in the worst case can be exponential in the number of prime factors, but we typically don't count output space in space complexity analysis - Excluding the output space, the auxiliary space complexity is
O(log n)
Common Pitfalls
1. Including Invalid Factorizations (1 and n as factors)
A frequent mistake is forgetting to exclude factorizations that include 1 or n itself as factors. The problem explicitly states factors must be in range [2, n-1].
Incorrect approach:
def getFactors(self, n: int) -> List[List[int]]:
def dfs(target, start):
# Wrong: Including [n] as a valid answer
result.append([target]) # This would include [n] itself
for factor in range(start, target + 1): # Wrong: allows factor = target
if target % factor == 0:
# process...
Why it's wrong: This would return [[8]] for n=8, which violates the constraint.
Solution: Only add combinations when current_path
is non-empty (ensuring at least one factorization has occurred), and the loop condition factor * factor <= target
naturally prevents using n as a factor.
2. Generating Duplicate Factorizations
Without maintaining non-decreasing order of factors, you'll get duplicates like [2, 3, 4] and [3, 2, 4] for n=24.
Incorrect approach:
def dfs(target, start):
if current_path:
result.append(current_path + [target])
# Wrong: Always starting from 2 instead of maintaining order
for factor in range(2, int(target**0.5) + 1):
if target % factor == 0:
current_path.append(factor)
dfs(target // factor, 2) # Wrong: resetting to 2
current_path.pop()
Why it's wrong: This allows factors to appear in any order, creating duplicates.
Solution: Pass the current factor as the starting point for the next recursion: dfs(target // factor, factor)
. This ensures factors are in non-decreasing order.
3. Incorrect Loop Boundary Leading to Missing Factorizations
Using the wrong upper bound in the factor loop can cause missed factorizations or infinite loops.
Incorrect approach:
def dfs(target, start):
if current_path:
result.append(current_path + [target])
# Wrong: using target-1 as upper bound
for factor in range(start, target):
if target % factor == 0:
current_path.append(factor)
dfs(target // factor, factor)
current_path.pop()
Why it's wrong: This explores unnecessary factors beyond sqrt(target) and could lead to performance issues. When factor > sqrt(target), the quotient target/factor < sqrt(target), meaning we've already explored this factorization from the other direction.
Solution: Use while factor * factor <= target
as the loop condition. This ensures we only check factors up to sqrt(target), avoiding redundant work while still finding all factorizations.
4. Modifying the Path List Incorrectly
Creating new lists instead of using backtracking can lead to incorrect results or memory issues.
Incorrect approach:
def dfs(target, start, path):
if path:
result.append(path + [target])
factor = start
while factor * factor <= target:
if target % factor == 0:
# Wrong: creating new list each time
dfs(target // factor, factor, path + [factor])
factor += 1
Why it's wrong: While this might work, it's inefficient as it creates many temporary lists. More importantly, if you try to modify path
directly without proper backtracking, you'll corrupt the path for other branches.
Solution: Use a single mutable list with proper backtracking:
current_path.append(factor) dfs(target // factor, factor) current_path.pop() # Critical: restore state for next iteration
What's the relationship between a tree and a graph?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!