DSA Guide
Dynamic Programming

Coin Change

Find the fewest coins that make a target amount, and see exactly why the obvious greedy approach is wrong

MediumDynamic Programming· unbounded knapsack
ArrayDynamic ProgrammingBFS
Problem #322

Problem Statement

Given coin denominations and a target amount, return the fewest coins needed to make that amount. You have an unlimited supply of each coin. If the amount cannot be made, return -1.

Input:  coins = [1, 2, 5], amount = 11
Output: 3
Why:    11 = 5 + 5 + 1

Input:  coins = [2], amount = 3
Output: -1

Input:  coins = [1], amount = 0
Output: 0

Intuition

The instinct is to take the largest coin that fits, repeatedly. With real currency that works, which is exactly why it is such a dangerous habit.

The counterexample that kills greedy

coins = [1, 3, 4],  amount = 6

Greedy: take 4  →  remaining 2
        take 1  →  remaining 1
        take 1  →  remaining 0
        Total: 3 coins  (4 + 1 + 1)

Best:   3 + 3  =  2 coins

Greedy takes the 4 because it is the biggest — and that single choice makes 3 + 3 unreachable.

Real coin systems (1, 5, 10, 25) are designed so greedy works. Arbitrary denominations are not.

Since no local rule can be trusted, every option has to be considered.

The key insight

Turn the question around. Instead of "which coin should I take first?", ask:

"To make amount a, what if the last coin I add is coin c?"

Then the rest of the work is making a - c, which is a smaller version of the same question. Try every coin as the last one and keep the best:

fewest(a) = 1 + min( fewest(a - c) for every coin c that fits )

Because fewest(a - c) is reused by many different a, this is dynamic programming rather than plain recursion.

Building from the bottom

Rather than recursing down from amount, build up from 0. To make 0 you need 0 coins — that is the anchor. Every later answer leans on earlier ones that are already final.

coins = [1, 2, 5]

amount:  0   1   2   3   4   5   6  ...
fewest:  0   1   1   2   2   1   2  ...

              3 = 2 + 1  →  1 + fewest(1) = 2

The real-world version

You are working out the fewest notes to pay a bill, and you have a notebook where you have already recorded the answer for every smaller amount.

For £11 you check each note you own: "if my last note is a £5, I still owe £6 — and I already know £6 takes 2 notes, so that path is 3." Do that for every note and keep the smallest.

Approach

Make a table of size amount + 1

fewest[a] will hold the minimum coins needed for amount a. You need index amount itself, hence the +1.

Fill it with an impossible value, except fewest[0] = 0

Use amount + 1 as the "impossible" marker — it is larger than any real answer, since even all-1-coins would need only amount of them.

fewest[0] = 0 is the anchor the rest is built on.

For each amount from 1 upward, try every coin

If coin c fits inside a, then one candidate answer is 1 + fewest[a - c]. Keep the smallest across all coins.

Read off fewest[amount]

If it is still the impossible marker, no combination works — return -1.

Why the impossible marker beats infinity

Using amount + 1 rather than Infinity keeps everything in plain integers and makes the final check a simple comparison.

It works because the largest possible real answer is amount (all ones), so amount + 1 can never be mistaken for a genuine result.

Watch it run

coins = [1, 2, 5], amount = 6 — the table being filled
0
0
a
1
-
2
-
3
-
4
-
5
-
6
-
noteanchor
fewest[0] = 0. Making nothing takes no coins. Everything else is unknown.
1 / 7
Each cell is computed once and reused by every larger amount that needs it.

Solution

def coin_change(coins: list[int], amount: int) -> int:
    """Fewest coins summing to amount, or -1 if impossible."""
    # amount + 1 is larger than any real answer, so it doubles as "impossible".
    IMPOSSIBLE = amount + 1
    fewest = [IMPOSSIBLE] * (amount + 1)

    # Anchor: making 0 needs 0 coins.
    fewest[0] = 0

    for value in range(1, amount + 1):
        for coin in coins:
            if coin <= value:
                # Use this coin as the last one, then reuse a solved subproblem.
                fewest[value] = min(fewest[value], 1 + fewest[value - coin])

    return fewest[amount] if fewest[amount] != IMPOSSIBLE else -1


def coin_change_bfs(coins: list[int], amount: int) -> int:
    """BFS view: each level is one more coin, so the first hit is the fewest."""
    from collections import deque

    if amount == 0:
        return 0

    queue = deque([0])
    seen = {0}
    steps = 0

    while queue:
        steps += 1
        for _ in range(len(queue)):
            total = queue.popleft()
            for coin in coins:
                nxt = total + coin
                if nxt == amount:
                    return steps
                if nxt < amount and nxt not in seen:
                    seen.add(nxt)
                    queue.append(nxt)

    return -1

Why BFS also works

Picture amounts as nodes and coins as edges. Every edge costs one coin, so all edges have equal weight — and BFS finds the shortest path in an unweighted graph.

The first time BFS reaches amount, it has used the fewest coins possible. It often runs faster than the table when the answer is small, because it stops the moment it arrives instead of filling every cell.

Complexity Analysis

Time Complexity

O(amount × coins)

Space Complexity

O(amount)

  • Time — the outer loop runs amount times and the inner loop once per coin. Every cell is computed exactly once.
  • Space — one array of amount + 1 entries. This cannot collapse to a few variables like House Robber, because fewest[a - c] may reach far back for a large coin.

The complexity depends on a VALUE, not a size

amount is a number in the input, not the length of anything. Doubling amount doubles the work even though the input has not grown.

This is called pseudo-polynomial. It matters in practice: amount = 10⁹ is unusable here, even with only three coins.

Edge Cases

  • Climbing Stairs — the same build-up, counting ways instead of minimising
  • House Robber — a DP whose window is small enough to collapse to two variables
  • Combination Sum — the same reuse rule, but listing every combination rather than the best
  • Greedy — when taking the local best is provably safe

On this page