title: Integer Break
slug: integer-break
difficulty: medium
leetcode_id: 343
leetcode_url: https://leetcode.com/problems/integer-break/
categories:
  - dynamic-programming
  - math
patterns:
  - dynamic-programming
  - greedy

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`)

    &nbsp;

    **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`

    &nbsp;

    **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

    &nbsp;

    **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

    &nbsp;

    **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.
