Facebook Pixel

3669. Balanced K-Factor Decomposition

Problem Description

You are given two integers n and k. Your task is to split the number n into exactly k positive integers such that when you multiply all these k integers together, the product equals n.

Among all possible ways to split n into k factors, you need to find the split where the difference between the largest and smallest number is minimized.

For example, if n = 12 and k = 3, you could split it as:

  • [1, 1, 12] with product = 1 × 1 × 12 = 12, difference = 12 - 1 = 11
  • [1, 2, 6] with product = 1 × 2 × 6 = 12, difference = 6 - 1 = 5
  • [1, 3, 4] with product = 1 × 3 × 4 = 12, difference = 4 - 1 = 3
  • [2, 2, 3] with product = 2 × 2 × 3 = 12, difference = 3 - 2 = 1

The optimal split would be [2, 2, 3] because it has the minimum difference of 1.

You should return any one valid split that achieves this minimum difference. The numbers can be returned in any order.

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

Intuition

To find k numbers whose product equals n with minimum difference between the largest and smallest, we need to make these numbers as close to each other as possible. Think about it: if we have factors that are very different in size (like 1 and n), the difference will be large. But if we can find factors closer to each other (like numbers around n^(1/k)), the difference will be smaller.

The key insight is that we need to explore all possible ways to factorize n into exactly k factors. This is essentially a recursive problem where at each step, we choose one factor of the remaining number and continue factorizing.

The approach pre-computes all divisors for each number up to a maximum value. For any number x, we store all its divisors in g[x]. This preprocessing step allows us to quickly access all possible factors during our search.

During the recursive search (dfs), we:

  1. Start with the number n and need to find k factors
  2. At each step, pick a divisor y of the current number x
  3. Continue recursively with x // y and one less factor to find
  4. Track the minimum and maximum factors seen so far to calculate the difference
  5. When we've found all k factors (when i == 0), we check if this split gives us a better (smaller) difference than what we've seen before

The recursion explores all possible factorizations, keeping track of the split that produces the minimum difference between the largest and smallest factors. By trying all combinations systematically, we guarantee finding the optimal solution where the factors are as balanced as possible.

Solution Approach

The solution uses a depth-first search (DFS) with preprocessing to find the optimal factorization.

Preprocessing Phase:

mx = 10**5 + 1
g = [[] for _ in range(mx)]
for i in range(1, mx):
    for j in range(i, mx, i):
        g[j].append(i)

This creates a list g where g[j] contains all divisors of j. For each number i from 1 to mx-1, we add i as a divisor to all its multiples (i, 2i, 3i, ...). This gives us O(1) access to all divisors of any number during the search.

DFS Implementation: The main algorithm uses recursive DFS to explore all possible factorizations:

def dfs(i: int, x: int, mi: int, mx: int):
  • i: Number of factors still needed (decrements from k-1 to 0)
  • x: Current number to be factorized
  • mi: Minimum factor found so far
  • mx: Maximum factor found so far

Base Case: When i == 0, we've found all k factors. The last factor must be x itself. We calculate the difference between the maximum and minimum factors (considering x as well):

d = max(mx, x) - min(mi, x)

If this difference is better than our current best, we update the answer.

Recursive Case: For each divisor y of the current number x:

  1. Add y to the current path at position i
  2. Recursively solve for x // y with one less factor needed
  3. Update the running minimum and maximum with y

Main Function:

ans = None
path = [0] * k
cur = inf
dfs(k - 1, n, inf, 0)
  • Initialize path array to store the current factorization being explored
  • cur tracks the best (minimum) difference found so far
  • Start DFS with k-1 factors to find (since the last one is determined), starting number n, and initial min/max values

The algorithm systematically explores all valid factorizations of n into exactly k factors, keeping track of the one with the minimum difference between its largest and smallest factors.

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 walk through the solution with n = 12 and k = 3.

Step 1: Preprocessing First, we build the divisor list g for all numbers up to 12:

  • g[12] = [1, 2, 3, 4, 6, 12] (all divisors of 12)
  • g[6] = [1, 2, 3, 6]
  • g[4] = [1, 2, 4]
  • g[3] = [1, 3]
  • g[2] = [1, 2]
  • g[1] = [1]

Step 2: DFS Exploration We start with dfs(i=2, x=12, mi=inf, mx=0) and need to find 3 factors.

The DFS explores different factorization paths:

Path 1: Choose divisor 1 first

  • path[2] = 1, call dfs(1, 12, 1, 1)
  • Choose divisor 1 again: path[1] = 1, call dfs(0, 12, 1, 1)
  • Base case: path[0] = 12
  • Result: [1, 1, 12], difference = 12 - 1 = 11

Path 2: Choose divisor 2 first

  • path[2] = 2, call dfs(1, 6, 2, 2)
  • Choose divisor 1: path[1] = 1, call dfs(0, 6, 1, 2)
  • Base case: path[0] = 6
  • Result: [1, 2, 6], difference = 6 - 1 = 5

Path 3: Choose divisor 2 first

  • path[2] = 2, call dfs(1, 6, 2, 2)
  • Choose divisor 2: path[1] = 2, call dfs(0, 3, 2, 2)
  • Base case: path[0] = 3
  • Result: [2, 2, 3], difference = 3 - 2 = 1 ✓ (Best so far!)

Path 4: Choose divisor 3 first

  • path[2] = 3, call dfs(1, 4, 3, 3)
  • Choose divisor 1: path[1] = 1, call dfs(0, 4, 1, 3)
  • Base case: path[0] = 4
  • Result: [1, 3, 4], difference = 4 - 1 = 3

The algorithm continues exploring all valid combinations. Each time a complete factorization is found (when i == 0), it compares the difference with the current best (cur). The factorization [2, 2, 3] gives the minimum difference of 1, so it becomes our final answer.

Step 3: Return Result The algorithm returns [2, 2, 3] as the optimal factorization where the product 2 × 2 × 3 = 12 and the difference between max and min is minimized to 1.

Solution Implementation

1from typing import List
2from math import inf
3
4# Precompute all divisors for numbers up to MAX_VALUE
5MAX_VALUE = 10**5 + 1
6divisors = [[] for _ in range(MAX_VALUE)]
7
8# Build divisors list: for each number i, add it as a divisor to all its multiples
9for divisor in range(1, MAX_VALUE):
10    for multiple in range(divisor, MAX_VALUE, divisor):
11        divisors[multiple].append(divisor)
12
13
14class Solution:
15    def minDifference(self, n: int, k: int) -> List[int]:
16        """
17        Find k factors of n such that the difference between max and min factor is minimized.
18      
19        Args:
20            n: The number to factorize
21            k: The number of factors to find
22          
23        Returns:
24            List of k factors that minimize the difference between max and min
25        """
26      
27        def backtrack(position: int, current_product: int, min_factor: int, max_factor: int) -> None:
28            """
29            Recursively find factors using backtracking.
30          
31            Args:
32                position: Current position in the factor array (working backwards from k-1 to 0)
33                current_product: The remaining product to factorize
34                min_factor: Minimum factor seen so far
35                max_factor: Maximum factor seen so far
36            """
37            nonlocal min_difference, best_factors
38          
39            # Base case: we've placed all factors
40            if position == 0:
41                # The last factor is the remaining product itself
42                final_min = min(min_factor, current_product)
43                final_max = max(max_factor, current_product)
44                difference = final_max - final_min
45              
46                # Update best solution if this one is better
47                if difference < min_difference:
48                    min_difference = difference
49                    current_factors[position] = current_product
50                    best_factors = current_factors[:]
51                return
52          
53            # Try each divisor of current_product as the next factor
54            for divisor in divisors[current_product]:
55                current_factors[position] = divisor
56                # Recurse with the quotient as the new product to factorize
57                backtrack(
58                    position - 1,
59                    current_product // divisor,
60                    min(min_factor, divisor),
61                    max(max_factor, divisor)
62                )
63      
64        # Initialize variables for tracking the best solution
65        best_factors = None
66        current_factors = [0] * k
67        min_difference = inf
68      
69        # Start backtracking from the last position with the full number n
70        backtrack(k - 1, n, inf, 0)
71      
72        return best_factors
73
1class Solution {
2    // Maximum value for n based on problem constraints
3    static final int MAX_VALUE = 100_001;
4  
5    // Pre-computed list of divisors for each number
6    // divisors[i] contains all divisors of i
7    static List<Integer>[] divisors = new ArrayList[MAX_VALUE];
8
9    // Static initialization block to pre-compute all divisors
10    static {
11        // Initialize ArrayList for each index
12        for (int i = 0; i < MAX_VALUE; i++) {
13            divisors[i] = new ArrayList<>();
14        }
15      
16        // For each potential divisor i, add it to all its multiples
17        // This efficiently builds divisor lists for all numbers
18        for (int divisor = 1; divisor < MAX_VALUE; divisor++) {
19            for (int multiple = divisor; multiple < MAX_VALUE; multiple += divisor) {
20                divisors[multiple].add(divisor);
21            }
22        }
23    }
24
25    // Current minimum difference found
26    private int currentMinDifference;
27  
28    // Best answer array found so far
29    private int[] bestAnswer;
30  
31    // Current path being explored in DFS
32    private int[] currentPath;
33
34    /**
35     * Finds k positive integers whose product equals n,
36     * minimizing the difference between max and min values
37     * 
38     * @param n the target product
39     * @param k the number of integers to find
40     * @return array of k integers with minimal max-min difference
41     */
42    public int[] minDifference(int n, int k) {
43        // Initialize tracking variables
44        currentMinDifference = Integer.MAX_VALUE;
45        bestAnswer = null;
46        currentPath = new int[k];
47      
48        // Start DFS from the last position, with remaining product n
49        dfs(k - 1, n, Integer.MAX_VALUE, 0);
50      
51        return bestAnswer;
52    }
53
54    /**
55     * Recursive DFS to explore all possible factorizations
56     * 
57     * @param position current position in the array (working backwards)
58     * @param remainingProduct product that still needs to be factored
59     * @param minValueSoFar minimum value selected so far
60     * @param maxValueSoFar maximum value selected so far
61     */
62    private void dfs(int position, int remainingProduct, int minValueSoFar, int maxValueSoFar) {
63        // Base case: reached the first position
64        if (position == 0) {
65            // The remaining product becomes the first element
66            currentPath[position] = remainingProduct;
67          
68            // Calculate the difference considering the new element
69            int difference = Math.max(maxValueSoFar, remainingProduct) - 
70                           Math.min(minValueSoFar, remainingProduct);
71          
72            // Update best answer if this difference is smaller
73            if (difference < currentMinDifference) {
74                currentMinDifference = difference;
75                bestAnswer = currentPath.clone();
76            }
77            return;
78        }
79      
80        // Try each divisor of the remaining product
81        for (int divisor : divisors[remainingProduct]) {
82            // Place this divisor at current position
83            currentPath[position] = divisor;
84          
85            // Recursively solve for the remaining positions
86            // with updated product and min/max values
87            dfs(position - 1, 
88                remainingProduct / divisor,
89                Math.min(minValueSoFar, divisor),
90                Math.max(maxValueSoFar, divisor));
91        }
92    }
93}
94
1class Solution {
2public:
3    // Maximum value constraint for the problem
4    static const int MAX_VALUE = 100001;
5  
6    // Static cache to store divisors for each number (computed once)
7    static vector<vector<int>> divisorsCache;
8
9    // Result variables
10    vector<int> answer;           // Final answer array
11    vector<int> currentPath;      // Current path being explored
12    int minDifference;            // Minimum difference found so far
13
14    vector<int> minDifference(int n, int k) {
15        // Initialize divisors cache only once (lazy initialization)
16        if (divisorsCache.empty()) {
17            initializeDivisorsCache();
18        }
19
20        // Initialize search variables
21        minDifference = INT_MAX;
22        answer.clear();
23        currentPath.assign(k, 0);
24
25        // Start DFS from the last position with the original number n
26        dfs(k - 1, n, INT_MAX, 0);
27        return answer;
28    }
29
30private:
31    // Precompute all divisors for numbers from 1 to MAX_VALUE
32    void initializeDivisorsCache() {
33        divisorsCache.resize(MAX_VALUE);
34      
35        // For each potential divisor i
36        for (int i = 1; i < MAX_VALUE; i++) {
37            // Add i as a divisor to all its multiples
38            for (int j = i; j < MAX_VALUE; j += i) {
39                divisorsCache[j].push_back(i);
40            }
41        }
42    }
43  
44    // DFS to find the path with minimum difference
45    // Parameters:
46    //   index: current position in the path (working backwards)
47    //   remainingValue: value to be factorized
48    //   minSoFar: minimum value in the current path
49    //   maxSoFar: maximum value in the current path
50    void dfs(int index, int remainingValue, int minSoFar, int maxSoFar) {
51        // Base case: reached the first position
52        if (index == 0) {
53            // Calculate difference including the last remaining value
54            int difference = max(maxSoFar, remainingValue) - min(minSoFar, remainingValue);
55          
56            // Update answer if we found a better solution
57            if (difference < minDifference) {
58                minDifference = difference;
59                currentPath[index] = remainingValue;
60                answer = currentPath;
61            }
62            return;
63        }
64      
65        // Try all divisors of the current remaining value
66        for (int divisor : divisorsCache[remainingValue]) {
67            currentPath[index] = divisor;
68          
69            // Recursively explore with:
70            // - decreased index
71            // - quotient as the new remaining value
72            // - updated min and max values
73            dfs(index - 1, 
74                remainingValue / divisor, 
75                min(minSoFar, divisor), 
76                max(maxSoFar, divisor));
77        }
78    }
79};
80
81// Static member initialization
82vector<vector<int>> Solution::divisorsCache;
83
1// Maximum value for precomputation
2const MAX_VALUE = 100001;
3
4// Precompute all divisors for each number from 1 to MAX_VALUE
5// divisors[i] contains all divisors of i
6const divisors: number[][] = Array.from({ length: MAX_VALUE }, () => []);
7
8// Build divisors array using sieve-like approach
9// For each number i, add it as a divisor to all its multiples
10for (let divisor = 1; divisor < MAX_VALUE; divisor++) {
11    for (let multiple = divisor; multiple < MAX_VALUE; multiple += divisor) {
12        divisors[multiple].push(divisor);
13    }
14}
15
16/**
17 * Finds k numbers whose product equals n, minimizing the difference between max and min values
18 * @param n - Target product value
19 * @param k - Number of factors needed
20 * @returns Array of k numbers whose product equals n with minimum difference
21 */
22function minDifference(n: number, k: number): number[] {
23    // Track the minimum difference found so far
24    let minDifferenceFound = Number.MAX_SAFE_INTEGER;
25  
26    // Store the best combination of factors found
27    let bestFactors: number[] | null = null;
28  
29    // Current path being explored in DFS
30    const currentPath: number[] = Array(k).fill(0);
31
32    /**
33     * Recursive DFS to find all valid factor combinations
34     * @param position - Current position in the path array (counting backwards from k-1)
35     * @param remainingProduct - Product value that still needs to be factored
36     * @param currentMin - Minimum value in current path
37     * @param currentMax - Maximum value in current path
38     */
39    function findFactors(
40        position: number, 
41        remainingProduct: number, 
42        currentMin: number, 
43        currentMax: number
44    ): void {
45        // Base case: reached the last factor
46        if (position === 0) {
47            // The last factor is the remaining product itself
48            const difference = Math.max(currentMax, remainingProduct) - Math.min(currentMin, remainingProduct);
49          
50            // Update best solution if this difference is smaller
51            if (difference < minDifferenceFound) {
52                minDifferenceFound = difference;
53                currentPath[position] = remainingProduct;
54                bestFactors = [...currentPath];
55            }
56            return;
57        }
58      
59        // Try each divisor of the remaining product
60        for (const divisor of divisors[remainingProduct]) {
61            currentPath[position] = divisor;
62          
63            // Recursively find factors for the quotient
64            findFactors(
65                position - 1,
66                Math.floor(remainingProduct / divisor),
67                Math.min(currentMin, divisor),
68                Math.max(currentMax, divisor)
69            );
70        }
71    }
72
73    // Start DFS from position k-1 with the original number n
74    findFactors(k - 1, n, Number.MAX_SAFE_INTEGER, 0);
75  
76    // Return the best factors found, or empty array if no solution exists
77    return bestFactors ?? [];
78}
79

Time and Space Complexity

Time Complexity: O(10^5 * d(10^5) + k * d(n)^k) where d(n) represents the number of divisors of n.

  • The preprocessing step builds a graph g where g[j] contains all divisors of j. For each number i from 1 to mx-1, we iterate through its multiples up to mx-1. This takes O(mx * log(mx)) time, which equals O(10^5 * log(10^5)) or approximately O(10^5 * 17).

  • The DFS explores all possible ways to decompose n into k factors. At each recursion level i, we iterate through all divisors of the current number x. In the worst case, we have k levels of recursion, and at each level, we branch into d(x) possibilities where d(x) is the number of divisors of x. The maximum number of divisors for numbers up to 10^5 is approximately 128. Therefore, the DFS has time complexity O(d(n)^k) in the worst case.

  • The overall time complexity is O(10^5 * log(10^5) + d(n)^k).

Space Complexity: O(10^5 * d_avg + k) where d_avg is the average number of divisors.

  • The graph g stores all divisors for each number from 1 to mx-1. Each number j stores all its divisors in g[j]. The total space used is the sum of divisors for all numbers up to 10^5, which is approximately O(10^5 * log(10^5)) or O(10^5 * 17) on average.

  • The recursion stack depth is k, and we maintain a path array of size k.

  • The overall space complexity is O(10^5 * log(10^5) + k).

Common Pitfalls

1. Inefficient Divisor Enumeration

One major pitfall is trying to find divisors on-the-fly during the search, which can be extremely slow:

Problematic approach:

def get_divisors(n):
    divisors = []
    for i in range(1, int(n**0.5) + 1):
        if n % i == 0:
            divisors.append(i)
            if i != n // i:
                divisors.append(n // i)
    return divisors

This takes O(√n) time for each call, and when called repeatedly during DFS, it becomes a bottleneck.

Solution: Precompute all divisors using the sieve-like approach shown in the code, which gives O(1) access to divisors during search.

2. Incorrect Pruning Logic

A tempting optimization is to prune branches early when the current difference exceeds the best found so far:

Incorrect pruning:

def backtrack(position, current_product, min_factor, max_factor):
    # Wrong: This prunes too aggressively
    if max_factor - min_factor >= min_difference:
        return  # This might miss better solutions!

This is incorrect because the final factor (when position == 0) could be within the current range and still improve the overall difference.

Correct approach: Only compare differences at the base case when all factors are determined.

3. Memory Limit Issues with Large Numbers

The precomputation array has a fixed size of 10^5 + 1. If n exceeds this limit, the code will crash:

Problem scenario:

n = 10**6  # Exceeds MAX_VALUE
k = 2
# This will cause IndexError: list index out of range

Solution: Either increase MAX_VALUE appropriately or implement a hybrid approach:

def minDifference(self, n: int, k: int) -> List[int]:
    if n >= MAX_VALUE:
        # Fall back to on-the-fly divisor calculation for large n
        return self.minDifferenceWithoutPrecompute(n, k)
    # Use precomputed approach for smaller n
    ...

4. Stack Overflow with Deep Recursion

For large values of k, the recursion depth can exceed Python's default limit:

Problem:

n = 2**30  # Large number with many factors of 2
k = 30     # Deep recursion
# May cause: RecursionError: maximum recursion depth exceeded

Solution: Either increase the recursion limit or convert to iterative approach:

import sys
sys.setrecursionlimit(10000)  # Increase limit

# Or use iterative approach with explicit stack
def minDifferenceIterative(self, n: int, k: int) -> List[int]:
    stack = [(k-1, n, inf, 0, [])]
    while stack:
        position, product, min_f, max_f, path = stack.pop()
        # Process similar to recursive version

5. Overlooking Edge Cases

The code doesn't handle certain edge cases explicitly:

Edge cases to consider:

  • k = 1: Should return [n]
  • n = 1: Should return [1, 1, ..., 1] (k times)
  • k > number of divisors of n: No valid solution exists

Enhanced solution with edge case handling:

def minDifference(self, n: int, k: int) -> List[int]:
    # Handle k = 1
    if k == 1:
        return [n]
  
    # Handle n = 1
    if n == 1:
        return [1] * k
  
    # Check if solution is possible (rough check)
    if k > 30 and n < 2**k:  # Impossible to have k factors
        return None  # Or raise exception
  
    # Continue with main algorithm...
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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

Load More