Coin systems
Greedy coin change: canonical coin systems, the greedy algorithm, when it fails and how unbounded coin-change dynamic programming compares.
Learning goals
- Apply greedy coin change and count the coins it uses
- Define a canonical coin system and test one by brute force
- Build counterexamples where greedy is suboptimal or gets stuck
- Compare greedy with the dynamic programming solution
Prerequisites
Making change is the problem every textbook uses to show that greedy sometimes works and sometimes fails, and the same coins reveal exactly what a proof has to rule out.
Canonical coin systems
A coin system is **canonical** if the greedy algorithm (take the largest coin that fits, repeat) gives the minimum number of coins for every amount. US coins 1, 5, 10, 25 are canonical, and so is any system in which each coin is a multiple of the next smaller one, such as powers of two. Systems like {1, 3, 4} and {1, 5, 10, 20, 25} are not.
Testing is finite: a non-canonical system with coins c1 < … < cn always has a counterexample amount smaller than c(n-1) + cn, so it is enough to compare greedy against the optimum for every amount below that bound. Doing so for {1, 5, 10, 20, 25} finds the smallest counterexample 40: greedy takes 25 + 10 + 5 for 3 coins, while 20 + 20 uses 2.
| Coin system | Canonical? | Why |
|---|---|---|
| 1, 5, 10, 25 | Yes | Each coin is checked against all amounts below the bound; no mismatch |
| 1, 2, 4, 8 | Yes | Each coin divides the next |
| 1, 3, 4 | No | Amount 6: greedy 4+1+1, optimal 3+3 |
| 1, 5, 10, 20, 25 | No | Amount 40: greedy 25+10+5, optimal 20+20 |
1// coins sorted ascending; returns the smallest amount where greedy is suboptimal, or -1 if canonical2static int firstCounterexample(int[] coins) {3int n = coins.length;4if (n < 2) return -1;5int limit = coins[n - 1] + coins[n - 2]; // a counterexample, if any, is below this6int[] best = new int[limit + 1];7Arrays.fill(best, Integer.MAX_VALUE);8best[0] = 0;9for (int a = 1; a <= limit; a++) {10for (int c : coins) if (c <= a && best[a - c] != Integer.MAX_VALUE) best[a] = Math.min(best[a], best[a - c] + 1);11int left = a, used = 0;12for (int i = n - 1; i >= 0; i--) { used += left / coins[i]; left %= coins[i]; }13if (left == 0 && used > best[a]) return a; // greedy used more coins than the optimum14}15return -1;16}
Greedy coin change vs the optimum
Interactive · edit the inputgreedy picks · remaining 6
Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin.
1static int greedyCoins(int[] coins, int amount) { // coins sorted in descending order; -1 if stuck2int count = 0;3for (int c : coins) {4count += amount / c; // as many of the biggest coin as fit5amount %= c;6}7return amount == 0 ? count : -1;8}
Live state
amount
6
coins
[4, 3, 1]
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | amount=6 coins=[4, 3, 1] | Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin. |
| 2 | L4 | coin=4 used=1 remaining=2 | 1 × 4 = 4: 2 remains. |
| 3 | L4 | coin=3 used=0 remaining=2 | 3 is larger than the remaining 2: skip it. |
| 4 | L4 | coin=1 used=2 remaining=0 | 2 × 1 = 2: 0 remains. |
| 5 | L7 | return=3 optimum=2 | Greedy used 3 coins but the optimum is 2: greedy is wrong for this coin system. |
Greedy coin change
The greedy algorithm: sort coins descending, and for each coin take as many as fit, then move on. For amount 63 with US coins that is two 25s, one 10 and three 1s: 6 coins, which the “Canonical system (US coins)” preset confirms equals the optimum. “Powers of two” makes 13 with 8 + 4 + 1.
The loop divides rather than subtracts one coin at a time, so it costs O(number of coin types). The function reports -1 when it ends with amount left over, which can only happen if the smallest coin is larger than 1 (the “Greedy gets stuck” preset).
1// coins sorted descending; returns the number of coins used, or -1 if the amount cannot be formed greedily2static int greedyCoins(int[] coinsDescending, int amount) {3int used = 0;4for (int c : coinsDescending) {5used += amount / c;6amount %= c;7}8return amount == 0 ? used : -1;9}
Best
O(k)
Average
O(k)
Worst
O(k)
Space
O(1)
Greedy coin change vs the optimum
Interactive · edit the inputgreedy picks · remaining 6
Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin.
1static int greedyCoins(int[] coins, int amount) { // coins sorted in descending order; -1 if stuck2int count = 0;3for (int c : coins) {4count += amount / c; // as many of the biggest coin as fit5amount %= c;6}7return amount == 0 ? count : -1;8}
Live state
amount
6
coins
[4, 3, 1]
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | amount=6 coins=[4, 3, 1] | Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin. |
| 2 | L4 | coin=4 used=1 remaining=2 | 1 × 4 = 4: 2 remains. |
| 3 | L4 | coin=3 used=0 remaining=2 | 3 is larger than the remaining 2: skip it. |
| 4 | L4 | coin=1 used=2 remaining=0 | 2 × 1 = 2: 0 remains. |
| 5 | L7 | return=3 optimum=2 | Greedy used 3 coins but the optimum is 2: greedy is wrong for this coin system. |
When greedy fails
Greedy can fail in two ways. It can be **suboptimal**: it forms the amount but uses too many coins ({1, 3, 4} and amount 6). It can also **get stuck**: it reaches a state with an amount that no remaining coin fits, although a different combination exists. The “Greedy gets stuck” preset uses coins {5, 4} and amount 8: greedy takes 5 and is left with 3, while 4 + 4 = 8.
Both failures come from the same cause: taking the biggest coin is a choice that cannot be undone, and the problem does not satisfy the greedy-choice property. The exchange argument breaks because replacing coins of a larger denomination by smaller ones can need a different number of coins. Use dynamic programming whenever the coin system is not known to be canonical.
Greedy coin change vs the optimum
Interactive · edit the inputgreedy picks · remaining 6
Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin.
1static int greedyCoins(int[] coins, int amount) { // coins sorted in descending order; -1 if stuck2int count = 0;3for (int c : coins) {4count += amount / c; // as many of the biggest coin as fit5amount %= c;6}7return amount == 0 ? count : -1;8}
Live state
amount
6
coins
[4, 3, 1]
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | amount=6 coins=[4, 3, 1] | Make 6 with the coins [4, 3, 1]. Greedy takes as many of the largest coin as fit, then moves to the next coin. |
| 2 | L4 | coin=4 used=1 remaining=2 | 1 × 4 = 4: 2 remains. |
| 3 | L4 | coin=3 used=0 remaining=2 | 3 is larger than the remaining 2: skip it. |
| 4 | L4 | coin=1 used=2 remaining=0 | 2 × 1 = 2: 0 remains. |
| 5 | L7 | return=3 optimum=2 | Greedy used 3 coins but the optimum is 2: greedy is wrong for this coin system. |
Unbounded coin change DP comparison
The dynamic programming solution computes the minimum number of coins for **every** amount from 0 up to the target: dp[a] = 1 + min(dp[a - c]) over the coins c not larger than a, with dp[0] = 0. It compares all choices of last coin instead of committing to the largest, so it is correct for any coin system.
The “Greedy fails: 3 + 3” preset fills the table for coins {1, 3, 4} and amount 6, ending with 2 coins, and traces the answer back as [3, 3]. The animation also counts combinations (the “Count combinations” preset) and reports an impossible amount as -1. The price for the correctness is O(amount × coins) time against greedy's O(coins).
1static int dpCoins(int[] coins, int amount) {2int[] dp = new int[amount + 1];3Arrays.fill(dp, Integer.MAX_VALUE);4dp[0] = 0;5for (int a = 1; a <= amount; a++) {6for (int c : coins) {7if (c <= a && dp[a - c] != Integer.MAX_VALUE) dp[a] = Math.min(dp[a], dp[a - c] + 1);8}9}10return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];11}
Best
O(amount × k)
Average
O(amount × k)
Worst
O(amount × k)
Space
O(amount)
Coin change: fewest coins, or number of ways
Interactive · edit the inputdp[a] = fewest coins
last coin used
dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c.
1static int minCoins(int[] coins, int amount) {2int INF = amount + 1;3int[] dp = new int[amount + 1]; Arrays.fill(dp, INF); dp[0] = 0; // dp[a] = fewest coins that make a4for (int a = 1; a <= amount; a++)5for (int c : coins)6if (c <= a && dp[a - c] + 1 < dp[a]) dp[a] = dp[a - c] + 1; // one coin c, then solve a - c7return dp[amount] >= INF ? -1 : dp[amount];8}9static long ways(int[] coins, int amount) {10long[] dp = new long[amount + 1]; dp[0] = 1; // one way to make 0: take nothing11for (int c : coins) // coins in the outer loop: each combination is counted once12for (int a = c; a <= amount; a++) dp[a] += dp[a - c];13return dp[amount];14}
Live state
amount
6
coins
[1, 3, 4]
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | amount=6 coins=[1, 3, 4] | dp[a] is the fewest coins that make a. dp[0] = 0, everything else starts at ∞ (not reachable yet). For each amount try every coin c: use one c and add 1 to the best for a − c. |
| 2 | L6 | a=1 coin=1 candidate=1 dp[a]=1 | Coin 1: dp[0] + 1 = 1 beats the old value: dp[1] = 1. |
| 3 | L6 | a=2 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[1] + 1 = 2 beats the old value: dp[2] = 2. |
| 4 | L6 | a=3 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[2] + 1 = 3 beats the old value: dp[3] = 3. |
| 5 | L6 | a=3 coin=3 candidate=1 dp[a]=1 | Coin 3: dp[0] + 1 = 1 beats the old value: dp[3] = 1. |
| 6 | L6 | a=4 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[3] + 1 = 2 beats the old value: dp[4] = 2. |
| 7 | L6 | a=4 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[1] + 1 = 2 does not improve dp[4] = 2. |
| 8 | L6 | a=4 coin=4 candidate=1 dp[a]=1 | Coin 4: dp[0] + 1 = 1 beats the old value: dp[4] = 1. |
| 9 | L6 | a=5 coin=1 candidate=2 dp[a]=2 | Coin 1: dp[4] + 1 = 2 beats the old value: dp[5] = 2. |
| 10 | L6 | a=5 coin=3 candidate=3 dp[a]=2 | Coin 3: dp[2] + 1 = 3 does not improve dp[5] = 2. |
| 11 | L6 | a=5 coin=4 candidate=2 dp[a]=2 | Coin 4: dp[1] + 1 = 2 does not improve dp[5] = 2. |
| 12 | L6 | a=6 coin=1 candidate=3 dp[a]=3 | Coin 1: dp[5] + 1 = 3 beats the old value: dp[6] = 3. |
| 13 | L6 | a=6 coin=3 candidate=2 dp[a]=2 | Coin 3: dp[3] + 1 = 2 beats the old value: dp[6] = 2. |
| 14 | L6 | a=6 coin=4 candidate=3 dp[a]=2 | Coin 4: dp[2] + 1 = 3 does not improve dp[6] = 2. |
| 15 | L7 | return=2 coins=[3, 3] | Fewest coins: 2. Following the last coin used from 6 down to 0 gives the coins [3, 3]. Other optimal combinations may exist; the traceback shows the one the table remembers. |
Practice ladder
Now try it yourself
Easy
- Lemonade Change
- Minimum Number of Operations to Convert Time
Medium
- Coin Change
- Coin Change II
Advanced
- Minimum Number of Coins for Fruits
- Patching Array
Key takeaways
- Greedy coin change takes the largest coin that fits; it is optimal only for canonical systems.
- Test a system by comparing greedy with dynamic programming for all amounts below the largest two coins' sum.
- Greedy can be suboptimal (1, 3, 4 for 6) or get stuck (5, 4 for 8).
- Dynamic programming is correct for every coin system at O(amount × coins) cost.