Greedy: always use the largest coin first. This fails — e.g., coins=[1,3,4], amount=6: greedy picks 4+1+1=3 coins, but 3+3=2 is optimal. Exhaustive recursion tries all combinations but is exponential.
Why greedy fails
Greedy assumes the largest coin always helps, but sometimes skipping a large coin leads to a better combination. Only DP or BFS over all possible remaining amounts guarantees the minimum.
Why we can do better
Problem: Recursion branches on each coin denomination at each step, giving O(Sⁿ) where S = amount/min_coin. Many subproblems overlap — minCoins(amount=3) is recomputed from multiple paths. Bottom-up DP fills a 1D array from 0 to amount, each cell O(coins) work, total O(amount × coins).
Java — Recursive (TLE)
classSolution {
publicintcoinChange(int[] coins, int amount) {
if (amount == 0) return0;
int min = Integer.MAX_VALUE;
for (int c : coins) {
if (c <= amount) {
int res = coinChange(coins, amount - c);
if (res >= 0) min = Math.min(min, res + 1);
}
}
return min == Integer.MAX_VALUE ? -1 : min;
}
}
Python — Recursive (TLE)
classSolution:
defcoinChange(self, coins: list[int], amount: int) -> int:
if amount == 0: return0
best = float('inf')
for c in coins:
if c <= amount:
res = self.coinChange(coins, amount - c)
if res >= 0: best = min(best, res + 1)
return best if best < float('inf') else -1
Metric
Value
Time
O(Sⁿ) where S = amount/min_coin
Space
O(amount) call stack
03
Section Three · Approach 2
Bottom-Up DP — O(amount × coins)
Build array dp[0..amount] where dp[a] = minimum coins for amount a. For each amount from 1 to target, try each coin: dp[a] = min(dp[a], dp[a - coin] + 1).
💡 Mental model: Imagine a vending machine change dispenser with an answer board on the wall. The board has slots labeled 0¢ to 100¢. Slot 0 says "0 coins." For each subsequent slot, you check: "If I use a quarter, the remaining is on slot 75 — that says 3 coins, so I'd need 4. If I use a dime, slot 90 says 1 coin, so I'd need 2." You write down the minimum for each slot. When you reach slot 100, you read the answer directly.
Algorithm
Create dp[amount+1], fill with amount+1 (impossible sentinel — any valid count ≤ amount).
dp[0] = 0.
For a from 1 to amount: for each coin, if coin ≤ a: dp[a] = min(dp[a], dp[a-coin] + 1).
Return dp[amount] if < amount+1, else -1.
🎯 When to recognize this pattern:
"Minimum items from unlimited supply to reach a target" = unbounded knapsack.
The signal: each item can be used multiple times, and you minimize (or count) combinations.
Also in LC 518 (Coin Change 2 — count combinations), LC 279 (Perfect Squares — treat squares as "coins").
Why amount+1 as sentinel instead of Integer.MAX_VALUE:
Using MAX_VALUE risks overflow when computing dp[a-coin] + 1.
Since no valid answer can exceed amount (worst case: all 1-cent coins), amount+1 is a safe, overflow-free sentinel.
import java.util.Arrays;
classSolution {
publicintcoinChange(int[] coins, int amount) {
int[] dp = newint[amount + 1];
Arrays.fill(dp, amount + 1); // sentinel — no valid answer > amount
dp[0] = 0;
for (int a = 1; a <= amount; a++)
for (int c : coins)
if (c <= a)
dp[a] = Math.min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
}
Python — Bottom-up DP
classSolution:
defcoinChange(self, coins: list[int], amount: int) -> int:
dp = [amount + 1] * (amount + 1) # sentinel
dp[0] = 0for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] <= amount else -1
06
Section Six · Analysis
Complexity Analysis
Approach
Time
Space
Trade-off
Greedy
O(amount/min)
O(1)
Wrong answer — greedy doesn't guarantee minimum
Recursive (brute force)
O(Sⁿ)
O(amount)
Correct but exponential
Bottom-up DP ← optimal
O(amount × n)
O(amount)
n = number of coin types; fills each cell once
BFS alternative: Treat each amount as a graph node and each coin as an edge. BFS from 0 to amount gives the shortest path = minimum coins. Same time/space as DP, but less intuitive for this problem. DP is the standard interview expectation.
⚠ Common Mistake: Using Integer.MAX_VALUE as the sentinel and then computing dp[a-c] + 1 — this overflows to Integer.MIN_VALUE, which becomes the "minimum" and corrupts the dp array. Always use amount + 1 as the sentinel since no valid answer exceeds amount.