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.
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:
- Start with the number
n
and need to findk
factors - At each step, pick a divisor
y
of the current numberx
- Continue recursively with
x // y
and one less factor to find - Track the minimum and maximum factors seen so far to calculate the difference
- When we've found all
k
factors (wheni == 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 fromk-1
to0
)x
: Current number to be factorizedmi
: Minimum factor found so farmx
: 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
:
- Add
y
to the current path at positioni
- Recursively solve for
x // y
with one less factor needed - 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 numbern
, 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 EvaluatorExample 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
, calldfs(1, 12, 1, 1)
- Choose divisor
1
again:path[1] = 1
, calldfs(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
, calldfs(1, 6, 2, 2)
- Choose divisor
1
:path[1] = 1
, calldfs(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
, calldfs(1, 6, 2, 2)
- Choose divisor
2
:path[1] = 2
, calldfs(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
, calldfs(1, 4, 3, 3)
- Choose divisor
1
:path[1] = 1
, calldfs(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
whereg[j]
contains all divisors ofj
. For each numberi
from 1 tomx-1
, we iterate through its multiples up tomx-1
. This takesO(mx * log(mx))
time, which equalsO(10^5 * log(10^5))
or approximatelyO(10^5 * 17)
. -
The DFS explores all possible ways to decompose
n
intok
factors. At each recursion leveli
, we iterate through all divisors of the current numberx
. In the worst case, we havek
levels of recursion, and at each level, we branch intod(x)
possibilities whered(x)
is the number of divisors ofx
. The maximum number of divisors for numbers up to10^5
is approximately 128. Therefore, the DFS has time complexityO(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 tomx-1
. Each numberj
stores all its divisors ing[j]
. The total space used is the sum of divisors for all numbers up to10^5
, which is approximatelyO(10^5 * log(10^5))
orO(10^5 * 17)
on average. -
The recursion stack depth is
k
, and we maintain apath
array of sizek
. -
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...
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
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!