A clear explanation of Coin Change using dynamic programming for minimum coin count.
Problem Restatement
We are given:
coins
amountcoins 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:
-1The 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 = 11One optimal choice:
5 + 5 + 1Coin count:
3Output:
3Example 2:
coins = [2]
amount = 3We cannot form 3 using only 2.
Output:
-1Example 3:
coins = [1]
amount = 0We need zero coins.
Output:
0First Thought: Greedy Choice
A natural idea is:
Always take the largest possible coin.
For:
coins = [1, 2, 5]
amount = 11this works:
5 + 5 + 1But greedy fails in general.
Example:
coins = [1, 3, 4]
amount = 6Greedy chooses:
4 + 1 + 1using 3 coins.
But the optimal answer is:
3 + 3using 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 - cSo:
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] = 0because zero coins are needed to form amount zero.
Initialize all other states as impossible.
infTransition
For every amount:
xtry every coin:
coinIf:
x >= cointhen we can form x by adding one coin after forming:
x - coinTransition:
dp[x] = min(dp[x], dp[x - coin] + 1)Example Walkthrough
Use:
coins = [1, 2, 5]
amount = 11Start:
dp[0] = 0All others:
infFor amount 1:
| Coin | Result |
|---|---|
1 | dp[0] + 1 = 1 |
So:
dp[1] = 1For amount 2:
| Coin | Result |
|---|---|
1 | dp[1] + 1 = 2 |
2 | dp[0] + 1 = 1 |
So:
dp[2] = 1For amount 5:
| Coin | Result |
|---|---|
1 | 5 coins |
2 | 3 coins |
5 | 1 coin |
So:
dp[5] = 1Eventually:
dp[11] = 3from:
5 + 5 + 1Algorithm
- 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] = 0is 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 - cand 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] + 1for 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] = 0Now 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 -1because 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 + cEvery 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 |