questions F-L

This commit is contained in:
2025-05-25 11:47:04 +01:00
parent ecf95bd23d
commit 917c371529
54 changed files with 11235 additions and 0 deletions

View 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`)
&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.