feat(content): test cases batch 2

This commit is contained in:
2025-05-24 21:00:16 +01:00
parent 1e4aafaff2
commit e8898841cf
10 changed files with 962 additions and 474 deletions

View File

@@ -10,90 +10,138 @@ patterns:
- dynamic-programming
description: |
You are given an integer array `coins` representing coins of different denominations and an
integer `amount` representing a total amount of money.
You are given an integer array `coins` representing coins of different denominations and an integer `amount` representing a total amount of money.
Return the fewest number of coins that you need to make up that amount. If that amount of
money cannot be made up by any combination of the coins, return -1.
Return *the fewest number of coins that you need to make up that amount*. If that amount of money cannot be made up by any combination of the coins, return `-1`.
You may assume that you have an infinite number of each kind of coin.
You may assume that you have an **infinite number** of each kind of coin.
constraints: |
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 2^31 - 1
- 0 <= amount <= 10^4
- `1 <= coins.length <= 12`
- `1 <= coins[i] <= 2^31 - 1`
- `0 <= amount <= 10^4`
examples:
- input: "coins = [1,2,5], amount = 11"
output: "3"
explanation: "11 = 5 + 5 + 1"
explanation: "11 = 5 + 5 + 1, using 3 coins."
- input: "coins = [2], amount = 3"
output: "-1"
explanation: "Cannot make amount 3 with only coin 2."
explanation: "Cannot make amount 3 with only coin denomination 2."
- input: "coins = [1], amount = 0"
output: "0"
explanation: "Amount 0 needs 0 coins."
explanation: "Amount 0 requires 0 coins."
explanation:
approach: |
1. Create a DP array where dp[i] = min coins needed for amount i
2. Initialize dp[0] = 0 (zero coins for zero amount)
3. For each amount from 1 to target, try each coin
4. If coin <= current amount, dp[i] = min(dp[i], dp[i - coin] + 1)
5. Return dp[amount] if valid, else -1
intuition: |
This is the classic unbounded knapsack problem. For each amount, we ask: "What's the
minimum coins needed if I use coin c as the last coin?"
Imagine you're at a vending machine that gives change. You want to give the customer exactly 11 cents using the fewest coins possible from denominations [1, 2, 5]. How do you think about this?
If we use coin c last, we need 1 + dp[amount - c] coins. We try all possible "last coins"
and take the minimum. This optimal substructure makes it perfect for DP.
Think of it like this: if I knew the minimum coins needed for amounts 0 through 10, I could figure out amount 11 by asking: "What if I use a 1-cent coin last? A 2-cent? A 5-cent?"
- Using 1-cent last: I need `coins(10) + 1`
- Using 2-cent last: I need `coins(9) + 1`
- Using 5-cent last: I need `coins(6) + 1`
The answer is the **minimum** of these options. This is the **optimal substructure** that makes dynamic programming work.
This is the classic **unbounded knapsack** pattern — "unbounded" because we can use each coin infinitely many times.
approach: |
We solve this using **Bottom-Up Dynamic Programming**:
**Step 1: Create and initialise the DP array**
- Create `dp` of size `amount + 1`, where `dp[i]` = minimum coins for amount `i`
- Initialise all values to infinity (or `amount + 1`) — meaning "impossible so far"
- Set `dp[0] = 0` as the base case: zero coins needed for amount zero
&nbsp;
**Step 2: Build up solutions for each amount**
- For each amount `i` from 1 to `amount`:
- Try each coin denomination
- If `coin <= i` (coin fits), check if using this coin improves our answer:
- `dp[i] = min(dp[i], dp[i - coin] + 1)`
- The `+1` accounts for using this coin; `dp[i - coin]` is the subproblem
&nbsp;
**Step 3: Return the answer**
- If `dp[amount]` is still infinity, return `-1` (impossible)
- Otherwise, return `dp[amount]`
&nbsp;
This builds solutions from smaller amounts to larger ones, ensuring we always have the subproblem solutions ready when we need them.
common_pitfalls:
- title: Wrong initialization
- title: Wrong Initialisation
description: |
Initialize dp array to infinity (or amount + 1), not 0.
dp[0] = 0 is the only base case.
wrong_approach: "Initializing all dp values to 0"
Initialising the DP array to `0` instead of infinity is a critical error. If `dp[5] = 0`, we'd incorrectly think amount 5 needs 0 coins!
We use `float('inf')` (or `amount + 1` as a practical upper bound) to represent "not yet achievable". Only `dp[0] = 0` should start as zero.
wrong_approach: "dp = [0] * (amount + 1)"
correct_approach: "dp = [float('inf')] * (amount + 1); dp[0] = 0"
- title: Not checking if subproblem is solvable
- title: Greedy Doesn't Work Here
description: |
Before using dp[i - coin], ensure i >= coin and dp[i - coin] is valid.
It's tempting to always use the largest coin first (greedy). But this fails!
- title: Returning wrong value for impossible case
**Counterexample**: `coins = [1, 3, 4]`, `amount = 6`
- Greedy: `4 + 1 + 1 = 6` (3 coins)
- Optimal: `3 + 3 = 6` (2 coins)
The greedy choice can block us from finding the actual minimum.
wrong_approach: "Always pick the largest coin that fits"
correct_approach: "Try all coins and take the minimum via DP"
- title: Not Checking for Impossible Cases
description: |
If dp[amount] is still infinity, return -1, not infinity.
If `dp[amount]` is still infinity after filling the table, no valid combination exists. Return `-1`, not infinity or some arbitrary value.
This happens when no coin divides evenly into the required amounts (e.g., `coins = [2]`, `amount = 3`).
wrong_approach: "return dp[amount]"
correct_approach: "return dp[amount] if dp[amount] != float('inf') else -1"
key_takeaways:
- Classic unbounded knapsack problem
- Bottom-up DP builds solution from smaller amounts
- Try each coin as the "last coin" for each amount
- Greedy doesn't work here (counterexample: coins=[1,3,4], amount=6)
- "**Unbounded knapsack pattern**: Each item (coin) can be used unlimited times — different from 0/1 knapsack"
- "**Greedy fails for coin change**: Classic counterexample shows why DP is necessary"
- "**Bottom-up builds confidence**: Solving smaller amounts first guarantees subproblems are ready"
- "**Foundation for variations**: This extends to counting combinations, finding exact change with fewest bills, etc."
time_complexity: "O(amount × coins)"
space_complexity: "O(amount)"
complexity_explanation: |
Time: For each amount (1 to target), we try each coin.
Space: DP array of size amount + 1.
time_complexity: "O(amount × n). For each amount from 1 to target, we try each of the n coins."
space_complexity: "O(amount). The DP array stores one value per amount from 0 to target."
solutions:
- approach_name: Bottom-Up DP (Optimal)
- approach_name: Bottom-Up DP
is_optimal: true
code: |
def coin_change(coins: list[int], amount: int) -> int:
# dp[i] = minimum coins needed for amount i
# Initialise to "impossible" (any value > amount works)
dp = [float('inf')] * (amount + 1)
# Base case: 0 coins needed for amount 0
dp[0] = 0
# Build solutions from amount 1 to target
for i in range(1, amount + 1):
# Try each coin as the "last coin" used
for coin in coins:
# Can only use this coin if it fits and subproblem is solvable
if coin <= i and dp[i - coin] != float('inf'):
dp[i] = min(dp[i], dp[i - coin] + 1)
# Return result, or -1 if impossible
return dp[amount] if dp[amount] != float('inf') else -1
explanation: |
Build up from amount 0. For each amount, try using each coin as the last coin.
Take the minimum of all valid options.
**Time Complexity:** O(amount × n) — For each of `amount` values, we check n coins.
**Space Complexity:** O(amount) — DP array of size `amount + 1`.
We build up from amount 0. For each amount, we try using each coin as the last coin and take the minimum. The key insight: if we know the minimum coins for smaller amounts, we can compute larger amounts by adding one coin at a time.
- approach_name: BFS (Alternative)
is_optimal: false
@@ -104,6 +152,7 @@ solutions:
if amount == 0:
return 0
# BFS: each "level" represents using one more coin
visited = {0}
queue = deque([(0, 0)]) # (current_sum, num_coins)
@@ -112,14 +161,21 @@ solutions:
for coin in coins:
next_sum = current + coin
# Found exact amount
if next_sum == amount:
return num_coins + 1
# Valid state we haven't seen
if next_sum < amount and next_sum not in visited:
visited.add(next_sum)
queue.append((next_sum, num_coins + 1))
# No valid combination found
return -1
explanation: |
BFS finds shortest path in unweighted graph.
First time we reach 'amount' is the minimum coins.
Less space-efficient than DP for this problem.
**Time Complexity:** O(amount × n) — Similar to DP in the worst case.
**Space Complexity:** O(amount) — Visited set can hold up to `amount` states.
BFS naturally finds the shortest path (minimum coins) in an unweighted graph. Each state is a sum, and edges represent adding a coin. The first time we reach `amount`, we've found the minimum. Generally less efficient than DP for this problem due to queue overhead.