questions F-L
This commit is contained in:
215
backend/data/questions/integer-break.yaml
Normal file
215
backend/data/questions/integer-break.yaml
Normal file
@@ -0,0 +1,215 @@
|
||||
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
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user