“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

Coin Change Problem

Algorithms

You've got a target amount and a list of coin denominations. Two related questions: what's the fewest coins that sum to , and how many distinct combinations of coins sum to ?

Dynamic programming approach

Keep one array, , of length . means different things depending on which version you're solving. For the fewest-coins question, is the minimum number of coins that sum to . For the number-of-ways question, is the count of distinct combinations that sum to . The algorithm is roughly the same in both cases. The only difference is the update rule for .

Time complexity

Each cell of fills in constant time by looking back at smaller cells. With coin denominations and target , the whole thing runs in .

Implementation

def coin_change_min_coins(coins, T):
    """Minimum number of coins that sum to T, or -1 if impossible."""
    dp = [float('inf')] * (T + 1)
    dp[0] = 0
    for x in range(1, T + 1):
        for coin in coins:
            if x >= coin:
                dp[x] = min(dp[x], dp[x - coin] + 1)
    return dp[T] if dp[T] != float('inf') else -1


def coin_change_num_ways(coins, T):
    """Count of distinct coin combinations that sum to T."""
    dp = [0] * (T + 1)
    dp[0] = 1
    for coin in coins:
        for x in range(coin, T + 1):
            dp[x] += dp[x - coin]
    return dp[T]


# Example: target 11 with coins {1, 2, 5}.
coins, T = [1, 2, 5], 11
print(coin_change_min_coins(coins, T))   # 3   (e.g., 5 + 5 + 1)
print(coin_change_num_ways(coins, T))    # 11

Try First

Each prompt asks a checkable question about the working code or math above — predict an output, derive a sign, state an invariant, find a bug. Commit to an answer before clicking "reveal." That commitment is the whole point: if your answer matched, you understand the piece you were looking at; if it didn't, that's the part worth re-reading.

invariant
In coin_change_num_ways, suppose the outer loop has finished processing the first k coins (so the next iteration would start for coin in coins[k:]). What does dp[x] mean at this moment? Be precise about which set of objects is being counted — including which coins are eligible and what orderings count as the same.
why does this work
coin_change_num_ways initializes dp[0] = 1. But there's no coin and no positive amount yet — what "way" is being counted? Predict what happens if you set dp[0] = 0 instead. Predict what happens if you set it to 2.
trace
Trace coin_change_min_coins([1, 2, 5], T=6) by writing out the full dp array as it gets filled. Note where each cell's value comes from. Then verify the answer matches your mental check: the fewest coins summing to 6 should be 2 (5 + 1) or (1 + 5) — same thing.
find the bug
For real US coins [1, 5, 10, 25], the greedy algorithm ("always take the largest coin that fits") gives the optimal answer. For [1, 3, 4] with T = 6, greedy gives 4 + 1 + 1 = 3 coins; the DP gives 3 + 3 = 2 coins. Construct another coin set where greedy fails, and articulate the property that distinguishes greedy-safe coin sets from greedy-unsafe ones.
predict
Without running the code, predict the order of magnitude of coin_change_num_ways([1, 2, 5], T=1000). Then predict T = 10000. Why does this count grow so fast in T?

Hack This

The code above works. Don't reinvent it — pull it into an editor, run it, and try the modifications below. Each one is small. Each one will change the behavior in a specific way; the question is which way.

trivial
Swap the loop order in coin_change_num_ways: change for coin in coins: for x in range(coin, T+1) to for x in range(1, T+1): for coin in coins:. Run it on coins=[1, 2, 5], T=5. The original returns 4; the swapped version returns 9. The function still works — but it's now counting something different. Write out the four (or nine) things being counted, and articulate the rule that distinguishes them.
small
Replace min with max in coin_change_min_coins and change the initial value from float('inf') to float('-inf'). Run on coins=[1, 2, 5], T=11. The answer becomes 11 (eleven 1-coins). When is "maximum number of coins" a meaningful business problem? And: what subtle thing happens if 1 isn't in your coin set?
small
min_coins returns the count but not which coins. Modify it to also return the actual list of coins that achieves the minimum. Use the standard DP-with-witness pattern: at each cell, record which coin's transition was chosen, then walk backward. Verify on coins=[1, 2, 5], T=11 — you should get [5, 5, 1] or any permutation totaling 11 with 3 coins.
small
Constrain the unbounded problem: you have k_i coins of denomination c_i, not unlimited. Modify num_ways to take a parallel list counts and respect it. This is the "bounded knapsack" version. What's the new time complexity? Can you still get O(T) space per coin, or does bookkeeping grow?
medium
Generalize min_coins in a direction it doesn't usually go: each coin has both a denomination c_i AND a value v_i. Maximize total value subject to total denomination equaling T exactly, with unlimited copies of each coin. You've just written unbounded knapsack. Notice the structural identity: the recurrence's shape is identical to coin change; only what you're optimizing has changed. Articulate the general principle.
medium
Compute num_ways([1, 2, 5], T=20) two ways: (a) using the DP above; (b) by extracting the coefficient of in the formal power series . Verify they agree. The DP is computing the same coefficient — just iteratively, by repeatedly multiplying by the next factor. Modify the code so that the connection to generating functions is visible in the structure (e.g., maintain an explicit polynomial as a list of coefficients, multiply one factor at a time).

Exercises

A full exercise set is available for this topic, structured as one worked example + 7 practice problems (across 7 surface contexts) + 2 pattern-resistant check problems.

Open the Coin Change & Dynamic Programming exercise set → Recognize a problem as having overlapping sub-problems and optimal substructure; identify a state variable and a recurrence that combines smaller solutions into larger ones; tabulate from the base case upward in O(states × choices) time.