Topics › Dynamic Programming › DP on Subsequences
Minimum Coins (DP - 20)
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array coins of distinct coin denominations and a target amount. You have an unlimited supply of every denomination. Return the fewest coins whose values add up to exactly amount, or -1 if no combination of coins makes it.
This is LeetCode 322, Coin Change. Constraints: 1 ≤ coins.length ≤ 12, 1 ≤ coins[i] ≤ 2^31 - 1, 0 ≤ amount ≤ 10^4.
Example 1
coins = [1, 2, 5], amount = 11
answer = 3 // 5 + 5 + 1
Example 2
coins = [2], amount = 3
answer = -1 // any number of 2s is even
Why greedy is not enough
With Indian or US currency, “take the largest coin that fits, repeat” happens to be optimal. For arbitrary denominations it is not. With coins = [1, 3, 4] and amount = 6, greedy takes 4 + 1 + 1 (three coins), but 3 + 3 uses two. Taking the big coin early can leave a remainder that only small coins can fill. We have to consider, for every denomination, how many copies to use, which is what the DP does.
The unbounded pick / not-pick recursion
This is the first problem in this series where an element may be used more than once. The state stays (i, t): denominations coins[0..i] are still allowed and t is the amount still to make. The only change from 0/1 subset problems is in the “take” branch:
- Not take coin
i: move on to the smaller denominations,(i - 1, t). - Take coin
ionce: pay 1 coin, and stay ati,(i, t - coins[i]), because we may want another copy.
Staying at i is what makes the supply unlimited. The recursion still terminates because t strictly decreases whenever we stay.
Base case at i = 0: only coins[0] is left, so t is reachable exactly when coins[0] divides it, using t / coins[0] coins. That covers t = 0 too (zero coins).
Impossibility is represented by INF = 10^9 rather than Integer.MAX_VALUE, so that 1 + INF cannot overflow into a negative number and win a min. Every method converts a final result ≥ INF into -1.
f(0, t) = t / coins[0] if coins[0] divides t, else INF
f(i, t) = min( f(i - 1, t), 1 + f(i, t - coins[i]) if coins[i] ≤ t )
answer = f(n - 1, amount), or -1 if that is INF
Approach 1: Recursion
Intuition. Implement the recurrence directly. Each call decides one thing: use another copy of coins[i], or never use coins[i] again.
Algorithm.
- If
i == 0, returnt / coins[0]if divisible, elseINF. notTake = f(i - 1, t).take = 1 + f(i, t - coins[i])ifcoins[i] ≤ t, elseINF.- Return
min(notTake, take).
static int recursive(int[] coins, int amount) {
int res = solve(coins.length - 1, amount, coins);
return res >= INF ? -1 : res;
}
static int solve(int i, int t, int[] coins) {
if (i == 0) return t % coins[0] == 0 ? t / coins[0] : INF; // only coins[0] left: it must divide t
int notTake = solve(i - 1, t, coins);
int take = coins[i] <= t ? 1 + solve(i, t - coins[i], coins) : INF; // stay on i: unlimited supply
return Math.min(notTake, take);
}
Complexity. Time is exponential: the depth can reach amount (when a coin of value 1 is taken repeatedly) and each call branches twice, so O(2^amount) is a safe bound. Space O(amount) for the recursion depth.
Approach 2: Memoization
Intuition. The answer for (i, t) does not depend on how we got there, and there are only n · (amount + 1) such states. Store each one. Real answers are ≥ 0 and INF is a real “impossible” value, so -1 is free to mean “not computed”.
Algorithm.
- Allocate
dp[n][amount + 1]filled with-1. - Same recursion; return the cached value when present, store every computed value.
- Convert
INFto-1at the end.
static int memoization(int[] coins, int amount) {
int[][] dp = new int[coins.length][amount + 1];
for (int[] row : dp) Arrays.fill(row, -1);
int res = memo(coins.length - 1, amount, coins, dp);
return res >= INF ? -1 : res;
}
static int memo(int i, int t, int[] coins, int[][] dp) {
if (i == 0) return t % coins[0] == 0 ? t / coins[0] : INF;
if (dp[i][t] != -1) return dp[i][t];
int notTake = memo(i - 1, t, coins, dp);
int take = coins[i] <= t ? 1 + memo(i, t - coins[i], coins, dp) : INF;
return dp[i][t] = Math.min(notTake, take);
}
Complexity. Time O(n · amount). Space O(n · amount) plus a stack that can be O(amount) deep. With a coin of value 1 and amount = 10^4, that is ten thousand nested calls, which may overflow Java’s default stack; the next two approaches have no such risk.
Approach 3: Tabulation
Intuition. dp[i][t] depends on dp[i - 1][t] (the row above) and dp[i][t - coins[i]] (the same row, further left). So fill row by row, and within a row from t = 0 upward; the cell to the left is then already final. Row 0 comes from the base case.
Compare with 0/1 problems, where “take” reads dp[i - 1][t - x], the row above. Reading the current row is the entire difference between “each item once” and “each item any number of times”.
Algorithm.
- Row 0:
dp[0][t] = t / coins[0]if divisible, elseINF. - For
i ≥ 1,tfrom 0 toamount:dp[i][t] = min(dp[i - 1][t], 1 + dp[i][t - coins[i]]), the second term only ifcoins[i] ≤ t. - Return
dp[n - 1][amount], or-1if it isINF.
static int tabulation(int[] coins, int amount) {
int n = coins.length;
int[][] dp = new int[n][amount + 1];
for (int t = 0; t <= amount; t++) dp[0][t] = t % coins[0] == 0 ? t / coins[0] : INF;
for (int i = 1; i < n; i++) {
for (int t = 0; t <= amount; t++) {
int notTake = dp[i - 1][t];
int take = coins[i] <= t ? 1 + dp[i][t - coins[i]] : INF; // same row: coin i may be used again
dp[i][t] = Math.min(notTake, take);
}
}
return dp[n - 1][amount] >= INF ? -1 : dp[n - 1][amount];
}
Complexity. Time O(n · amount), space O(n · amount).
Approach 4: Space optimized
Intuition. Collapse the rows into one array best. Before processing coin c, best[t] holds the row above; processing t from left to right turns it into the current row, because best[t - c] has already been updated for c and so already allows copies of c. This is the opposite direction from the 0/1 subset problems, for exactly the reason described above.
We can also start from a cleaner base: best[0] = 0 and everything else INF means “with no coins at all, only 0 is reachable”; processing coins[0] first then reproduces row 0.
Algorithm.
best[0] = 0, every other entryINF.- For each coin
c, fortfromctoamount:best[t] = min(best[t], 1 + best[t - c]). - Return
best[amount], or-1if it isINF.
static int spaceOptimized(int[] coins, int amount) {
int[] best = new int[amount + 1];
Arrays.fill(best, INF);
best[0] = 0; // zero coins make amount 0
for (int c : coins) {
for (int t = c; t <= amount; t++) { // left to right: best[t - c] may already use c
best[t] = Math.min(best[t], 1 + best[t - c]);
}
}
return best[amount] >= INF ? -1 : best[amount];
}
Dry run on Example 1 (coins = [1, 2, 5], amount = 11), showing best[0..11] after each coin:
| after coin | best[0..11] |
|---|---|
| 1 | 0 1 2 3 4 5 6 7 8 9 10 11 |
| 2 | 0 1 1 2 2 3 3 4 4 5 5 6 |
| 5 | 0 1 1 2 2 1 2 2 3 3 2 3 |
best[11] = 3, achieved as best[6] + 1 with best[6] = 2 (5 + 1), so the coins are 5 + 5 + 1.
Complexity. Time O(n · amount), space O(amount).
Edge cases and pitfalls
amount = 0. The answer is 0 coins, not-1. The base case0 % coins[0] == 0andbest[0] = 0both give this.- No solution. Return
-1, notINFand not 0.[5, 7]with amount 3 has every coin too large. - Overflow.
Integer.MAX_VALUEas “impossible” turns into a negative number after+ 1.INF = 10^9leaves room. A coin of value2^31 - 1is safe because the guards comparecoins[i] ≤ tinstead of computingt - coins[i]first. - Loop direction. In the 1D version, right-to-left would allow each coin at most once and give wrong answers such as
-1for[2]with amount 4. - Greedy. As shown above, it fails for denominations like
[1, 3, 4]. - Testing. The Java file cross-checks all four methods on 200 seeded random coin sets.
Related problems
- Coin Change 2 (DP - 22) counts the combinations instead of minimising the coins.
- Unbounded knapsack uses the same “stay on the item after taking it” recursion to maximise value.
- Rod Cutting Problem - (DP - 24) is unbounded knapsack with piece lengths as weights.
- Minimum coins is the SDE sheet’s greedy version for canonical currency systems.
- Subset sum equal to target (DP- 14) is the 0/1 counterpart, where each element can be used once.