Coin Change
Interview Prep
The problem
Given a list of coin denominations (with unlimited supply of each)
and a target amount, return the minimum number of coins needed to
make the amount, or −1 if impossible.
Why greedy fails
Take the largest coin that fits, repeat. This works for US currency
but not in general. Counter-example: coins
[1, 3, 4], amount 6. Greedy takes 4 then 1 + 1 = 3 coins.
Optimal is 3 + 3 = 2 coins. The "biggest first" rule is locally
optimal but globally wrong whenever a smaller coin enables a much
better pairing.
def coin_change_greedy(coins, amount):
"""WRONG in general — only works for canonical coin systems (e.g., US)."""
coins = sorted(coins, reverse=True)
count = 0
for c in coins:
while amount >= c:
amount -= c
count += 1
return count if amount == 0 else -1
# coins=[1,3,4], amount=6 -> greedy: 4+1+1 = 3 coins
# optimal: 3+3 = 2 coins Greedy works iff the coin system is "canonical" — a property that can be tested in polynomial time but is not in general guaranteed. Don't bet on it.
Pattern: unbounded knapsack DP
Let dp[a] be the minimum coins to make amount
a. The recurrence is:
dp[a] = 1 + min(dp[a − c]) over all coins
c ≤ a. Base case dp[0] = 0. This is the
"unbounded" knapsack flavor — each coin can be used any number of
times, so we iterate amounts outer, coins inner, and read from
dp[a − c] (same row).
Solution
def coin_change(coins: list[int], amount: int) -> int:
INF = amount + 1
dp = [INF] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a and dp[a - c] + 1 < dp[a]:
dp[a] = dp[a - c] + 1
return dp[amount] if dp[amount] != INF else -1 Trace
coins = [1, 3, 4], amount = 6
dp[0] = 0
dp[1] = min(dp[0]+1) = 1 [1]
dp[2] = min(dp[1]+1) = 2 [1,1]
dp[3] = min(dp[2]+1, dp[0]+1) = 1 [3]
dp[4] = min(dp[3]+1, dp[1]+1, dp[0]+1) = 1 [4]
dp[5] = min(dp[4]+1, dp[2]+1, dp[1]+1) = 2 [4,1] or [1,1,3]
dp[6] = min(dp[5]+1, dp[3]+1, dp[2]+1) = 2 [3,3]
return dp[6] = 2 Complexity
- Time:
O(amount · k)wherek = |coins|. - Space:
O(amount).
This is pseudo-polynomial: it's polynomial in the numerical value of
amount, not in the number of bits to represent it. For
huge amounts with few coins, BFS on the implicit graph (states are
amounts, edges are coin subtractions) can be faster in practice
because it stops at the first time the target is reached.
Variations worth knowing
- Number of ways (Coin Change II): count the distinct ways to make the amount. Loop order matters! Iterate coins outer, amounts inner — otherwise you count permutations instead of combinations.
- Reconstruct the coin set: store
parent[a] = cwhen you updatedp[a]; walk back from amount. - Bounded coin counts: if each coin has a finite supply, it becomes the 0/1 knapsack flavor — iterate amounts in reverse to prevent reusing a coin.
- Coin Change with infinite denominations queried online:
precompute
dpup to the largest query.