LeetCode 322: Coin Change
A clear explanation of Coin Change using dynamic programming for minimum coin count.
Problem Restatement
We are given:
coins
amount
coins contains coin denominations.
We may use each coin any number of times.
Return the minimum number of coins needed to make exactly amount.
If it is impossible, return:
-1
The official constraints include:
| Constraint | Value |
|---|---|
1 <= coins.length <= 12 |
|
1 <= coins[i] <= 2^31 - 1 |
|
0 <= amount <= 10^4 |
Input and Output
| Item | Meaning |
|---|---|
| Input | Coin denominations and target amount |
| Output | Minimum number of coins |
| Unlimited reuse | Yes |
| Impossible case | Return -1 |
Function shape:
def coinChange(coins: list[int], amount: int) -> int:
...
Examples
Example 1:
coins = [1, 2, 5]
amount = 11
One optimal choice:
5 + 5 + 1
Coin count:
3
Output:
3
Example 2:
coins = [2]
amount = 3
We cannot form 3 using only 2.
Output:
-1
Example 3:
coins = [1]
amount = 0
We need zero coins.
Output:
0
First Thought: Greedy Choice
A natural idea is:
Always take the largest possible coin.
For:
coins = [1, 2, 5]
amount = 11
this works:
5 + 5 + 1
But greedy fails in general.
Example:
coins = [1, 3, 4]
amount = 6
Greedy chooses:
4 + 1 + 1
using 3 coins.
But the optimal answer is:
3 + 3
using only 2 coins.
So we need dynamic programming.
Key Insight
Suppose we already know the minimum coins needed for smaller amounts.
For amount x, if we use coin c last, then before using c we must already have formed:
x - c
So:
dp[x] = min(dp[x - c] + 1)
over all usable coins c.
This gives a direct recurrence.
DP Definition
Let:
dp[x]
mean:
Minimum number of coins needed to form amount
x.
Base case:
dp[0] = 0
because zero coins are needed to form amount zero.
Initialize all other states as impossible.
inf
Transition
For every amount:
x
try every coin:
coin
If:
x >= coin
then we can form x by adding one coin after forming:
x - coin
Transition:
dp[x] = min(dp[x], dp[x - coin] + 1)
Example Walkthrough
Use:
coins = [1, 2, 5]
amount = 11
Start:
dp[0] = 0
All others:
inf
For amount 1:
| Coin | Result |
|---|---|
1 |
dp[0] + 1 = 1 |
So:
dp[1] = 1
For amount 2:
| Coin | Result |
|---|---|
1 |
dp[1] + 1 = 2 |
2 |
dp[0] + 1 = 1 |
So:
dp[2] = 1
For amount 5:
| Coin | Result |
|---|---|
1 |
5 coins |
2 |
3 coins |
5 |
1 coin |
So:
dp[5] = 1
Eventually:
dp[11] = 3
from:
5 + 5 + 1
Algorithm
- Create a DP array of size
amount + 1. - Set all values to infinity.
- Set:
dp[0] = 0 - For each amount from
1toamount:- Try every coin.
- Update the minimum value.
- If
dp[amount]is still infinity, return-1. - Otherwise return
dp[amount].
Correctness
We prove by induction on the amount.
Base case:
dp[0] = 0
is correct because zero coins are needed to form amount zero.
Now assume all values:
dp[0], dp[1], ..., dp[x - 1]
are correct.
To form amount x, consider the last coin used in an optimal solution. Suppose that coin is c.
Then the remaining amount is:
x - c
and the number of coins used before the last coin must be:
dp[x - c]
by the induction hypothesis.
Therefore the optimal solution for x has value:
dp[x - c] + 1
for some valid coin c.
The algorithm checks every possible coin and takes the minimum, so it computes exactly the optimal number of coins for amount x.
Thus every dp[x] is correct, including dp[amount].
If no combination can form the target, the value remains infinity and the algorithm correctly returns -1.
Complexity
Let:
| Symbol | Meaning |
|---|---|
n |
Number of coin types |
A |
Target amount |
| Metric | Value | Why |
|---|---|---|
| Time | O(nA) |
Every amount tries every coin |
| Space | O(A) |
One DP array |
Implementation
class Solution:
def coinChange(
self,
coins: list[int],
amount: int,
) -> int:
dp = [float("inf")] * (amount + 1)
dp[0] = 0
for current in range(1, amount + 1):
for coin in coins:
if current >= coin:
dp[current] = min(
dp[current],
dp[current - coin] + 1,
)
if dp[amount] == float("inf"):
return -1
return dp[amount]
Code Explanation
Create the DP array.
dp = [float("inf")] * (amount + 1)
Every amount is initially impossible.
Base case:
dp[0] = 0
Now compute amounts from small to large.
for current in range(1, amount + 1):
Try every coin.
for coin in coins:
If the coin can fit:
if current >= coin:
then update the answer.
dp[current] = min(
dp[current],
dp[current - coin] + 1,
)
At the end:
if dp[amount] == float("inf"):
return -1
because no valid combination exists.
Otherwise return the minimum coin count.
BFS Interpretation
This problem can also be viewed as shortest path search.
Each amount is a node.
From amount x, using coin c moves to:
x + c
Every edge has cost 1.
The goal is to reach amount using the fewest steps.
Dynamic programming is usually simpler and faster here.
Testing
def run_tests():
s = Solution()
assert s.coinChange([1, 2, 5], 11) == 3
assert s.coinChange([2], 3) == -1
assert s.coinChange([1], 0) == 0
assert s.coinChange([1], 2) == 2
assert s.coinChange([2, 5, 10, 1], 27) == 4
assert s.coinChange([186, 419, 83, 408], 6249) == 20
assert s.coinChange([3, 4], 6) == 2
print("all tests passed")
run_tests()
Test meaning:
| Test | Why |
|---|---|
[1,2,5], 11 |
Standard example |
[2], 3 |
Impossible case |
[1], 0 |
Base case |
[1], 2 |
Repeated reuse |
| Mixed coins | Larger reachable target |
| Large official-style test | Performance check |
[3,4], 6 |
Greedy would fail with other coin systems |