150NeetCode 150

106.Coin Change

MediumBlind 751-D Dynamic Programming ยท 8/12

Problem

You are given coin denominations coins and a target amount. You have an unlimited supply of each coin.

Return the fewest coins whose values add up to exactly amount, or -1 if no combination works. An amount of 0 needs 0 coins.

Examples

Input:  coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1.
Input:  coins = [2], amount = 3
Output: -1
Explanation: Only even totals are reachable.
Input:  coins = [1], amount = 0
Output: 0

Constraints

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

๐Ÿ’ก Key Insight

Greedy (largest coin first) fails โ€” with coins = [1, 3, 4] and amount = 6, greedy gives 4 + 1 + 1 but 3 + 3 is better. Instead, think about the last coin used: if it was c, the rest is an optimal answer for amount - c. So f(a) = 1 + min over coins c <= a of f(a - c), with f(0) = 0 and f(a) = โˆž when nothing fits. There are only amount + 1 distinct subproblems, so a 1-D table of size amount + 1 filled bottom-up solves it in O(amount ร— coins).

Recursion (Brute Force)

TimeO(k^(amount / minCoin))SpaceO(amount / minCoin)

Intuition

To make a, try every coin as the next coin and recursively make the remainder; keep the minimum. Negative remainders are dead ends.

Recurrence: f(0) = 0; f(a) = min(1 + f(a - c)) over all c in coins with c <= a; f(a) = โˆž if no coin leads to a finite answer.

Algorithm

  1. dfs(a): if a == 0 return 0.
  2. best = โˆž; for each c <= a: sub = dfs(a - c); if finite, best = min(best, sub + 1).
  3. Return best; the caller maps โˆž to -1.

Visualization

coins = [1, 2, 5], amount = 5 (partial tree). f(3), f(2) and f(1) show up in several branches.

flowchart TD
    A["f(5)"] -->|"-1"| B["f(4)"]
    A -->|"-2"| C["f(3)"]
    A -->|"-5"| D["f(0) = 0"]
    B -->|"-1"| E["f(3)"]
    B -->|"-2"| F["f(2)"]
    C -->|"-1"| G["f(2)"]
    C -->|"-2"| H["f(1)"]
    E -->|"-1"| I["f(2)"]
    E -->|"-2"| J["f(1)"]
    F -->|"-2"| K["f(0) = 0"]

The -5 branch gives f(5) = 1 immediately, but plain recursion still explores every other branch in full.

Code

java
class Solution {
    private static final int INF = Integer.MAX_VALUE;

    public int coinChange(int[] coins, int amount) {
        int result = dfs(coins, amount);
        return result == INF ? -1 : result;
    }

    // fewest coins to make exactly a, or INF if impossible
    private int dfs(int[] coins, int a) {
        if (a == 0) {
            return 0;
        }
        int best = INF;
        for (int c : coins) {
            if (c <= a) {
                int sub = dfs(coins, a - c);
                if (sub != INF) {
                    best = Math.min(best, sub + 1);
                }
            }
        }
        return best;
    }
}

Complexity

  • Time: O(k^(amount / minCoin)) โ€” k = coins.length branches at every level, exponential.
  • Space: O(amount / minCoin) โ€” recursion depth (amount when a coin of 1 exists).

Complexity Comparison

Approach Time Space
Recursion (Brute Force) O(k^amount) O(amount)
Top-Down Memoization O(amount ร— k) O(amount)
Bottom-Up Tabulation O(amount ร— k) O(amount)

Common Pitfalls

  • Using a greedy "biggest coin first" strategy โ€” wrong for denominations like [1, 3, 4].
  • Using Integer.MAX_VALUE as infinity and then computing dp[a - c] + 1, which overflows to a negative number. Use amount + 1 or check before adding.
  • Forgetting amount = 0 โ†’ 0, not -1.
  • In memoization, conflating "not computed" with "impossible" โ€” they need different sentinels, or impossible states get recomputed forever.
  • Coins can be as large as 2^31 - 1; compare c <= a before subtracting rather than computing a - c blindly.