Coin change problem – Count Ways
Given coin denominations and a target sum, with an unlimited supply of each denomination, count the distinct combinations of coins that add up to the target. Two combinations that use the same coins in a different order are not counted separately.
Do this lesson first: coin changeExample input
coins = [1, 3, 4], amount = 5
Expected output
3
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
Row 0 is the only base row. With no coins allowed, the only combination is the empty one, so dp[0][0] = 1 and every other cell in row 0 is 0. Column 0 computes, and is 1 all the way down for the same reason: taking no coins makes 0 whichever coins are allowed. Fill row by row and left to right within a row. The use-at-least-one case reads a cell to the LEFT in the SAME row, so that cell has to be final first, which a left-to-right sweep guarantees. Apart from adding where subset-sum takes the larger, that same-row read is what separates this table from subset-sum's, which reads the row above because each number there may be used only once.
Cols →
Rows ↓row i: may use the first i coins
Cols →amount s: combinations that make s
- 1
function countWays(coins, amount) { - 2
const n = coins.length; - 3
const dp = Array.from({ length: n + 1 }, () => new Array(amount + 1).fill(0)); - 4
dp[0][0] = 1; - 5
for (let i = 1; i <= n; i++) { - 6
const c = coins[i - 1]; - 7
for (let s = 0; s <= amount; s++) { - 8
dp[i][s] = dp[i - 1][s]; - 9
if (c <= s) dp[i][s] += dp[i][s - c]; - 10
} - 11
} - 12
return dp[n][amount]; - 13
}
The code, the trap, the variations
- 1
function countWays(coins, amount) { - 2
const n = coins.length; - 3
const dp = Array.from({ length: n + 1 }, () => new Array(amount + 1).fill(0)); - 4
dp[0][0] = 1; - 5
for (let i = 1; i <= n; i++) { - 6
const c = coins[i - 1]; - 7
for (let s = 0; s <= amount; s++) { - 8
dp[i][s] = dp[i - 1][s]; - 9
if (c <= s) dp[i][s] += dp[i][s - c]; - 10
} - 11
} - 12
return dp[n][amount]; - 13
}
Where people go wrong
Counting orders as different combinations. The natural first attempt carries the flagship lesson's one-row recurrence across with a sum in place of 1 + min and ways[0] = 1, ways[s] = the sum over coins c <= s of ways[s - c], and that counts sequences, not combinations. At amount 5 with coins {1, 3, 4} it gives 6, counting 3 + 1 + 1, 1 + 3 + 1 and 1 + 1 + 3 separately and 4 + 1 and 1 + 4 separately, where the answer is 3. The gap grows quickly: at amount 11 it is 104 against 9. Dividing out the duplicates afterwards has no clean formula. The fix is to decide one coin at a time, which is what the rows of this table do.
Count ordered sequences instead, so a 1 then a 3 differs from a 3 then a 1.
Drop the coin axis: one row over amounts, dp[s] = the sum over coins c <= s of dp[s - c]. That is climbing-stairs with the coins as step sizes. The second axis on this page exists to stop exactly that.
Allow each coin to be used at most once.
The use-it case reads the row above, dp[i - 1][s - c_i], instead of the current row. State and fill order stay; it is subset-sum with a count in place of a yes or no.
Ask for the fewest coins instead of the number of combinations.
That is coin-change-minimum-coins and the flagship lesson. A min replaces the sum and 1 + is added to the use-it case. Row 0 changes too: dp[0][0] = 0 coins, and every other amount is unreachable (infinity) rather than 0 ways. Because order does not change how many coins you used, the one-row recurrence becomes correct again and the coin axis can go.