Facebook Pixel

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 as 2 × 2 × 2
  • 8 can also be expressed as 2 × 4

Both of these factorizations would be valid answers.

The key requirements are:

  1. Each combination should multiply together to equal n
  2. All factors in a combination must be at least 2 and at most n-1
  3. You need to find ALL possible combinations, not just one
  4. The order of combinations in your output doesn't matter
  5. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 up
  • ans: The result list that collects all valid factorizations

Algorithm Walkthrough:

  1. Recording Valid Factorizations: At the start of each dfs call, if t is non-empty (meaning we have at least one factor), we record a valid factorization by appending t + [n] to our answer. This captures factorizations where n is the last factor.

  2. Exploring Factors: We iterate through potential factors starting from i (initially 2). The loop continues while j * j <= n because:

    • If j > sqrt(n), then n/j < sqrt(n), meaning we've already considered this factorization from the other side
    • This optimization prevents duplicate work and ensures termination
  3. Recursive Decomposition: When we find a valid factor j (where n % j == 0):

    • Add j to our current path t
    • 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 from t
  4. Backtracking Pattern: The t.append(j) followed by t.pop() implements the classic backtracking pattern:

    • Make a choice (add factor j)
    • Explore recursively with that choice
    • Undo the choice to try other possibilities

Example Trace for n=12:

  • Start with dfs(12, 2), t = []
  • Try factor 2: t = [2], call dfs(6, 2)
    • In dfs(6, 2): record [2, 6]
    • Try factor 2: t = [2, 2], call dfs(3, 2)
      • In dfs(3, 2): record [2, 2, 3]
    • Try factor 3: t = [2, 3], call dfs(2, 3)
      • In dfs(2, 3): record [2, 3, 2] → Actually this won't happen since 3 * 3 > 2
  • Try factor 3: t = [3], call dfs(4, 3)
    • In dfs(4, 3): record [3, 4]

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 Evaluator

Example 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 from i to sqrt(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 approximately O(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 most logâ‚‚(n) levels
  • The temporary list t: O(log n) - At most contains logâ‚‚(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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More