feat(content): test cases batch 2
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
**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
|
||||
|
||||
|
||||
|
||||
**Step 3: Return the answer**
|
||||
|
||||
- If `dp[amount]` is still infinity, return `-1` (impossible)
|
||||
- Otherwise, return `dp[amount]`
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user