A clear explanation of Integer Break using dynamic programming, with a note on the greedy math solution.
Problem Restatement
We are given an integer n.
We must break n into the sum of at least two positive integers.
Among all possible breaks, return the maximum product of those integers.
For example:
10 = 3 + 3 + 4The product is:
3 * 3 * 4 = 36So for n = 10, the answer is 36.
The constraints are:
2 <= n <= 58The problem statement asks to break n into k positive integers where k >= 2, then maximize the product.
Input and Output
| Item | Meaning |
|---|---|
| Input | Integer n |
| Output | Maximum product after breaking n |
| Break rule | Must use at least two positive integers |
| Constraint | 2 <= n <= 58 |
Function shape:
def integerBreak(n: int) -> int:
...Examples
Example 1:
Input: n = 2
Output: 1Explanation:
2 = 1 + 1
1 * 1 = 1Example 2:
Input: n = 10
Output: 36Explanation:
10 = 3 + 3 + 4
3 * 3 * 4 = 36First Thought: Try Every Break
A direct recursive idea is:
For every possible first part x, recursively solve the remaining value n - x.
But there is a detail.
After we split n into:
x + (n - x)we have two choices for the second part:
- Keep
n - xas one number. - Break
n - xfurther.
So the best product for this split is:
x * max(n - x, best product after breaking n - x)This naturally leads to dynamic programming.
Key Insight
Let:
dp[i] = maximum product obtainable by breaking integer iFor each i, try every first part x from 1 to i - 1.
The remaining part is:
i - xFor that remaining part, we choose the better of:
| Choice | Product |
|---|---|
| Do not break it further | x * (i - x) |
| Break it further | x * dp[i - x] |
So the transition is:
dp[i] = max(dp[i], x * (i - x), x * dp[i - x])This works because the original number must be broken at least once, but after that, each remaining part may or may not be broken further.
Algorithm
Create an array dp of length n + 1.
Set:
dp[1] = 1Then for every i from 2 to n:
- Try every split
x + (i - x). - Compare:
x * (i - x)x * dp[i - x]
- Store the best value in
dp[i].
Return:
dp[n]Walkthrough
Use:
n = 10Some important values:
dp[2] = 1 from 1 + 1
dp[3] = 2 from 1 + 2
dp[4] = 4 from 2 + 2
dp[5] = 6 from 2 + 3
dp[6] = 9 from 3 + 3
dp[7] = 12 from 3 + 4
dp[8] = 18 from 3 + 3 + 2
dp[9] = 27 from 3 + 3 + 3
dp[10] = 36 from 3 + 3 + 4For i = 10, one split is:
3 + 7If we do not break 7, product is:
3 * 7 = 21If we break 7 optimally:
dp[7] = 12then product is:
3 * 12 = 36So dp[10] becomes 36.
Correctness
The algorithm considers every possible first split of each integer i.
For a fixed split:
i = x + (i - x)there are exactly two relevant choices for the second part.
It can remain whole, giving:
x * (i - x)or it can be broken further, giving:
x * dp[i - x]The value dp[i - x] is already correct because i - x < i, and the table is filled from smaller values to larger values.
Taking the maximum over all first parts x therefore considers every valid way to break i.
The base value handles the smallest subproblem.
Since dp[n] is computed from all valid breaks of n, it equals the maximum product obtainable after breaking n into at least two positive integers.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n^2) | For each i, we try up to i - 1 splits |
| Space | O(n) | The dp array stores one value for each integer |
Given n <= 58, this is easily fast enough.
Implementation
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0] * (n + 1)
dp[1] = 1
for total in range(2, n + 1):
for first in range(1, total):
rest = total - first
dp[total] = max(
dp[total],
first * rest,
first * dp[rest],
)
return dp[n]Code Explanation
Create the DP table:
dp = [0] * (n + 1)Set the smallest useful value:
dp[1] = 1Now compute products for each total:
for total in range(2, n + 1):Try every possible first number:
for first in range(1, total):The remaining number is:
rest = total - firstThen compare the two choices:
first * restmeans we stop splitting the remaining part.
first * dp[rest]means we split the remaining part optimally.
The best value is stored in:
dp[total]Greedy Math Version
There is also a shorter mathematical solution.
For maximum product, we want to use as many 3s as possible, except when the remainder would be 1.
Why?
For values larger than 4, splitting off a 3 improves or preserves the product.
But a remainder of 1 is bad because:
3 * 1 < 2 * 2So if the remainder is 1, replace one 3 + 1 with 2 + 2.
Implementation:
class Solution:
def integerBreak(self, n: int) -> int:
if n == 2:
return 1
if n == 3:
return 2
product = 1
while n > 4:
product *= 3
n -= 3
return product * nThis version runs in O(n) time with the loop, or O(1) if written with exponentiation.
Testing
def run_tests():
s = Solution()
assert s.integerBreak(2) == 1
assert s.integerBreak(3) == 2
assert s.integerBreak(4) == 4
assert s.integerBreak(5) == 6
assert s.integerBreak(6) == 9
assert s.integerBreak(10) == 36
assert s.integerBreak(12) == 81
assert s.integerBreak(58) == 1549681956
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
2 | Smallest input, must split into 1 + 1 |
3 | Must split, so answer is 2, not 3 |
4 | Best split is 2 + 2 |
5 | Best split is 2 + 3 |
6 | Best split is 3 + 3 |
10 | Standard sample |
12 | All 3s |
58 | Upper constraint |