LeetCode 3669 - Balanced K-Factor Decomposition

The problem asks us to split a positive integer n into exactly k positive integers such that the product of these integers equals n. Among all possible splits, we are required to find one in which the maximum difference between any two numbers is minimized.

LeetCode Problem 3669

Difficulty: 🟡 Medium
Topics: Math, Backtracking, Number Theory

Solution

Problem Understanding

The problem asks us to split a positive integer n into exactly k positive integers such that the product of these integers equals n. Among all possible splits, we are required to find one in which the maximum difference between any two numbers is minimized.

In simpler terms, we want to factorize n into k numbers that are as "balanced" as possible so that the largest and smallest factors are as close as possible. The input integers n and k define the size of the number to split and the number of factors, respectively. The output is a list of k integers that satisfy the conditions.

Constraints are important here: 4 <= n <= 10^5 ensures that n is relatively small, so factorization is feasible. 2 <= k <= 5 is tiny, which implies we can afford to enumerate factor combinations without hitting performance issues. The constraint that k is less than the total number of positive divisors guarantees that a solution always exists.

Edge cases to consider include when n is prime, when k is 2 (simplest split), and when n is a perfect power allowing evenly balanced factors.

Approaches

The brute-force approach would involve generating all combinations of k positive integers whose product is n and then computing the max-min difference for each combination. This guarantees correctness because it checks every possibility. However, it is infeasible even for moderate n because the number of factor combinations grows rapidly with n.

The key observation for an optimal approach is that minimizing the difference between the largest and smallest numbers is equivalent to making the factors as close as possible to the k-th root of n. This is because, mathematically, the geometric mean of k numbers with a fixed product is minimized in difference when all numbers are equal or as close as possible. Using this insight, we can generate all factors of n, then recursively attempt to split n into k factors, always trying the smallest available factors first to balance the numbers.

Approach Time Complexity Space Complexity Notes
Brute Force O(P^k) O(k) Generate all k-combinations of factors; infeasible for large n
Optimal O(d^(k-1)) O(k) Backtracking on sorted factor list; k <= 5 ensures feasibility

Here d is the number of divisors of n, which is relatively small for n <= 10^5.

Algorithm Walkthrough

  1. Factor Generation: First, generate all factors of n. This is done by iterating up to sqrt(n) and collecting both divisors from n % i == 0. This ensures we have all candidate numbers for splitting.
  2. Sorting Factors: Sort the factors in ascending order. This allows us to prioritize smaller numbers first to balance the split.
  3. Backtracking: Use a recursive backtracking approach to try splitting n into k factors. Maintain a temporary array for the current split and recursively reduce the remaining product and the number of factors left to fill.
  4. Pruning: If at any point the remaining product cannot be divided among the remaining slots (either too large or too small compared to available factors), backtrack immediately. This reduces unnecessary computation.
  5. Minimizing Difference: Track the max-min difference for each valid split. If a new split has a smaller difference, update the result.
  6. Return Result: Once the recursion completes, return the split with the minimum max-min difference.

Why it works: The algorithm systematically explores all factor combinations in a controlled manner, leveraging the small k to ensure feasibility. The pruning and factor ordering ensure we focus on balanced splits first, which naturally minimizes the maximum difference.

Python Solution

from typing import List
import math

class Solution:
    def minDifference(self, n: int, k: int) -> List[int]:
        factors = []
        for i in range(1, int(math.isqrt(n)) + 1):
            if n % i == 0:
                factors.append(i)
                if i != n // i:
                    factors.append(n // i)
        factors.sort()
        
        result = []
        min_diff = float('inf')
        
        def backtrack(start: int, k_left: int, product: int, path: List[int]):
            nonlocal min_diff, result
            if k_left == 1:
                if product in factors:
                    candidate = path + [product]
                    diff = max(candidate) - min(candidate)
                    if diff < min_diff:
                        min_diff = diff
                        result = candidate
                return
            for i in range(start, len(factors)):
                f = factors[i]
                if product % f == 0:
                    backtrack(i, k_left - 1, product // f, path + [f])
        
        backtrack(0, k, n, [])
        return result

Implementation Walkthrough: We first generate all factors, then sort them. backtrack recursively tries each factor starting from the current index, ensuring no duplicates. When only one factor is left, we check if it divides the remaining product exactly. Each valid split updates the minimum difference and the result.

Go Solution

import (
    "math"
    "sort"
)

func minDifference(n int, k int) []int {
    factors := []int{}
    for i := 1; i*i <= n; i++ {
        if n%i == 0 {
            factors = append(factors, i)
            if i != n/i {
                factors = append(factors, n/i)
            }
        }
    }
    sort.Ints(factors)
    
    result := []int{}
    minDiff := math.MaxInt32
    
    var backtrack func(start, kLeft, product int, path []int)
    backtrack = func(start, kLeft, product int, path []int) {
        if kLeft == 1 {
            for _, f := range factors {
                if f == product {
                    candidate := append(append([]int{}, path...), f)
                    maxVal, minVal := candidate[0], candidate[0]
                    for _, v := range candidate {
                        if v > maxVal {
                            maxVal = v
                        }
                        if v < minVal {
                            minVal = v
                        }
                    }
                    if maxVal-minVal < minDiff {
                        minDiff = maxVal - minVal
                        result = append([]int{}, candidate...)
                    }
                    break
                }
            }
            return
        }
        for i := start; i < len(factors); i++ {
            f := factors[i]
            if product%f == 0 {
                backtrack(i, kLeft-1, product/f, append(path, f))
            }
        }
    }
    
    backtrack(0, k, n, []int{})
    return result
}

Go-specific Notes: Slices are used instead of Python lists, and care is taken to copy slices before modifying result to avoid shared references. math.MaxInt32 is used for initial comparison. Integer division is safe due to factor checking.

Worked Examples

Example 1: n = 100, k = 2

Step Remaining Product Path Action
Start 100 [] Try factor 2
Next 50 [2] Only one left, check if 50 in factors -> yes
Candidate [2,50] max-min = 48 Update result
Next 100 [] Try factor 4
Candidate [4,25] max-min = 21 Update result
Next 100 [] Try factor 10
Candidate [10,10] max-min = 0 Update result, optimal

Final output: [10,10].

Example 2: n = 44, k = 3

Backtracking explores [1,1,44], [1,2,22], [1,4,11], [2,2,11], finds [2,2,11] with min difference 9.

Complexity Analysis

Measure Complexity Explanation
Time O(d^(k-1)) For each factor we recursively explore k-1 levels; d is number of factors of n
Space O(k + d) Path recursion stack is at most k, storing all factors requires O(d) space

Since k <= 5 and d is small for n <= 10^5, this is feasible.

Test Cases

# Provided examples
assert Solution().minDifference(100, 2) == [10,10]  # minimal difference 0
assert Solution().minDifference(44, 3) == [2,2,11]  # minimal difference 9

# Edge and boundary cases
assert Solution().minDifference(4, 2) == [2,2]      # smallest n, k=2
assert Solution().minDifference(5, 2) == [1,5]      # prime n
assert Solution().minDifference(81, 4) == [3,3,3,3] # perfect power, balanced split
assert Solution().minDifference(1000, 3)            # split 1000 into 3 balanced factors
assert Solution().minDifference(100, 5)             #