title: Integer Break slug: integer-break difficulty: medium leetcode_id: 343 leetcode_url: https://leetcode.com/problems/integer-break/ categories: - dynamic-programming - math patterns: - slug: dynamic-programming is_optimal: true - slug: greedy is_optimal: false function_signature: "def integer_break(n: int) -> int:" test_cases: visible: - input: { n: 2 } expected: 1 - input: { n: 10 } expected: 36 hidden: - input: { n: 3 } expected: 2 - input: { n: 4 } expected: 4 - input: { n: 5 } expected: 6 - input: { n: 6 } expected: 9 - input: { n: 7 } expected: 12 - input: { n: 8 } expected: 18 - input: { n: 11 } expected: 54 - input: { n: 58 } expected: 1549681956 description: | Given an integer `n`, break it into the sum of `k` **positive integers**, where `k >= 2`, and maximize the product of those integers. Return *the maximum product you can get*. constraints: | - `2 <= n <= 58` examples: - input: "n = 2" output: "1" explanation: "2 = 1 + 1, 1 x 1 = 1." - input: "n = 10" output: "36" explanation: "10 = 3 + 3 + 4, 3 x 3 x 4 = 36." explanation: intuition: | Imagine you have a rope of length `n` and you must cut it into at least two pieces. You want the **product of the piece lengths** to be as large as possible. The key mathematical insight is: **3s are magical**. When you break a number into parts, using 3s (with some 2s for adjustment) produces the maximum product. Why? Consider that for any number greater than 4, breaking off a 3 gives a better product than keeping it whole. For example, `6` as a single piece contributes 6 to the product, but `3 + 3` contributes `3 x 3 = 9`. Think of it like this: you're trying to pack as many 3s as possible because they're the most "efficient" multiplier. The exceptions are: - If the remainder is 1, you should take one 3 back and make two 2s instead (because `2 x 2 = 4 > 3 x 1 = 3`) - If the remainder is 2, just keep it as a 2 This greedy insight can also be approached with dynamic programming, where we build up optimal products for smaller numbers. approach: | We can solve this using either a **Mathematical (Greedy)** approach or **Dynamic Programming**. The math approach is O(1), but DP helps understand the structure. **Mathematical Approach:** **Step 1: Handle base cases** - If `n == 2`: Return `1` (must split into `1 + 1`) - If `n == 3`: Return `2` (must split into `1 + 2`, giving `1 x 2 = 2`)   **Step 2: Divide n by 3 to determine the split** - If `n % 3 == 0`: Use all 3s. The answer is `3^(n/3)` - If `n % 3 == 1`: Use one fewer 3 and add two 2s. The answer is `3^(n/3 - 1) x 4` - If `n % 3 == 2`: Use all 3s plus one 2. The answer is `3^(n/3) x 2`   **Dynamic Programming Approach:** **Step 1: Initialise the DP array** - Create array `dp` of size `n + 1` where `dp[i]` represents the maximum product for integer `i` - Set `dp[1] = 1` as the base case   **Step 2: Fill the DP table** - For each `i` from `2` to `n`: - Try every possible first cut `j` from `1` to `i - 1` - The product is either `j x (i - j)` (if we don't break further) or `j x dp[i - j]` (if we continue breaking) - Take the maximum across all cuts   **Step 3: Return the result** - Return `dp[n]` common_pitfalls: - title: Forgetting Base Cases description: | For `n = 2` and `n = 3`, the optimal "unforced" choice would be to not break at all, but the problem requires at least 2 pieces. For `n = 2`: Must return `1` (from `1 + 1`) For `n = 3`: Must return `2` (from `1 + 2`), not `3` When using DP, remember that `dp[2]` and `dp[3]` used as subproblems can return their full value (2 and 3), since the "must break" constraint only applies to the original number. wrong_approach: "Returning 2 for n=2 or 3 for n=3" correct_approach: "Handle n=2 and n=3 as special cases with forced splits" - title: Not Considering Both Options in DP description: | When computing `dp[i]`, for each cut position `j`, you must consider two options: - `j x (i - j)`: Don't break the remaining part further - `j x dp[i - j]`: Break the remaining part optimally Missing the first option means you miss cases where not breaking further is optimal. For example, when `i = 4` and `j = 2`, the answer is `2 x 2 = 4`, not `2 x dp[2] = 2 x 1 = 2`. wrong_approach: "Only considering j x dp[i - j]" correct_approach: "max(j x (i - j), j x dp[i - j]) for each j" - title: Using 1s in the Split description: | Including 1 in your split is almost always suboptimal. For any factor of 1, you could add that 1 to another factor to increase the product. For example, `3 + 3 + 1` gives `3 x 3 x 1 = 9`, but `3 + 4` gives `3 x 4 = 12`. The only time 1 appears is in forced base cases (`n = 2` and `n = 3`). wrong_approach: "Splitting into parts that include 1" correct_approach: "Only use 2s and 3s (except for base cases)" key_takeaways: - "**The power of 3**: For maximising products, 3 is the optimal factor (provable via calculus or discrete analysis)" - "**Greedy meets math**: Sometimes a mathematical insight replaces the need for DP entirely, reducing O(n^2) to O(1)" - "**DP transition structure**: The pattern of choosing whether to break further (`j x (i-j)` vs `j x dp[i-j]`) appears in many partition problems" - "**Related problems**: This connects to *Cutting a Rod*, *Partition Equal Subset Sum*, and other optimisation over partitions" time_complexity: "O(1) for the mathematical approach. O(n^2) for dynamic programming, as we compute each `dp[i]` by iterating through all possible cuts." space_complexity: "O(1) for the mathematical approach. O(n) for dynamic programming to store the `dp` array." solutions: - approach_name: Mathematical (Greedy) is_optimal: true code: | def integer_break(n: int) -> int: # Base cases: must split, but would prefer not to if n == 2: return 1 # 1 + 1 = 2, product = 1 if n == 3: return 2 # 1 + 2 = 3, product = 2 # For n >= 4, use as many 3s as possible if n % 3 == 0: # n is divisible by 3, use all 3s return 3 ** (n // 3) elif n % 3 == 1: # Remainder 1: take one 3 back, use 2 + 2 instead # (because 2 x 2 = 4 > 3 x 1 = 3) return 3 ** (n // 3 - 1) * 4 else: # Remainder 2: use all 3s plus one 2 return 3 ** (n // 3) * 2 explanation: | **Time Complexity:** O(1) — Just arithmetic operations (exponentiation is O(log n) but with small exponents here). **Space Complexity:** O(1) — Only a few variables used. The mathematical insight is that 3 is the optimal factor. For any `n >= 5`, breaking off a 3 and multiplying gives a larger product than keeping the number whole. We handle remainders: if `n % 3 == 1`, we use `2 + 2` instead of `3 + 1` since `4 > 3`. - approach_name: Dynamic Programming is_optimal: false code: | def integer_break(n: int) -> int: # dp[i] = maximum product for integer i dp = [0] * (n + 1) dp[1] = 1 # Base case for i in range(2, n + 1): for j in range(1, i): # Option 1: don't break (i - j) further # Option 2: break (i - j) optimally using dp product = max(j * (i - j), j * dp[i - j]) dp[i] = max(dp[i], product) return dp[n] explanation: | **Time Complexity:** O(n^2) — For each `i` from 2 to n, we try all cuts from 1 to i-1. **Space Complexity:** O(n) — We store the dp array of size n+1. For each number `i`, we try every possible first cut `j`. The remaining part `i - j` can either stay whole (giving `j * (i - j)`) or be broken further (giving `j * dp[i - j]`). We take the maximum across all possibilities. - approach_name: Recursion with Memoization is_optimal: false code: | def integer_break(n: int) -> int: memo = {} def helper(num: int, must_break: bool) -> int: # If we've computed this before, return cached result if (num, must_break) in memo: return memo[(num, must_break)] # Base case if num <= 1: return num # If we don't have to break, we can return num itself if not must_break: result = num else: result = 0 # Try all possible first cuts for i in range(1, num): # First piece is i, remaining is num - i (which can stay whole) product = i * helper(num - i, False) result = max(result, product) memo[(num, must_break)] = result return result # Start with must_break=True since we need at least 2 pieces return helper(n, True) explanation: | **Time Complexity:** O(n^2) — Each subproblem is solved once, and each takes O(n) to compute. **Space Complexity:** O(n) — Memoization cache plus recursion stack. This top-down approach explicitly tracks whether we're forced to break. The original call has `must_break=True`, but recursive calls for remaining parts use `must_break=False` since they can stay whole if that's optimal.