Problem overview
The method Change receives a target amount and an array coins of distinct coin values. It returns how many different combinations of coins add up to exactly amount, or 0 if there are none. Each coin value can be used any number of times. A combination is defined by how many coins of each value it uses, not by their order, so 2 + 2 + 1 and 1 + 2 + 2 are the same combination.
For amount = 5 and coins = {1, 2, 5} the answer is 4: 5, 2 + 2 + 1, 2 + 1 + 1 + 1 and 1 + 1 + 1 + 1 + 1. For amount = 3 and coins = {2} the answer is 0, since 2s only make even sums. When amount is 0 the answer is 1: the empty combination.
The lesson asks for O(n · amount) time and O(amount) space, where n is the number of coins.
Approach: count the ways for every smaller sum
Listing combinations one by one is hopeless: a few dozen coin values can already produce over a billion combinations of 5000. Instead, count them. Let ways[a] be the number of combinations that make the sum a, for every a from 0 to amount. The answer is ways[amount].
The table starts with ways[0] = 1 (no coins, sum 0) and zeros everywhere else, because with no coins available no positive sum can be made. Then the coins are added one at a time. When a coin is added, every combination for a - coin, plus one more of this coin, is a combination for a. So ways[a] grows by ways[a - coin].
Two choices in that loop decide the result:
- Sums go upward. While handling a coin,
ways[a - coin]has already been updated for the same coin, so it may contain that coin once or more. This is what lets one coin value be used again and again. - Coins are the outer loop. When a coin is added, the table only knows combinations built from the coins before it. Each combination is therefore built in one fixed order: all its coins of the first value, then of the second, and so on. It is counted exactly once.
1 + 3is counted and3 + 1never is.
Swapping the loops, with the sums outside and the coins inside, counts orders instead of combinations. For amount = 4 and coins = {1, 2, 3} the correct loop order returns 4, and the swapped one returns 7, because it counts 3 + 1 and 1 + 3 separately.
The table, coin by coin
For amount = 5 and coins = {1, 2, 5}, these are the contents of ways (indices 0 to 5) after each coin:
after coin 1: 1 1 1 1 1 1
after coin 2: 1 1 2 2 3 3
after coin 5: 1 1 2 2 3 4
After the coin 1, every sum has one way: all ones. After the coin 2, the sum 4 has three ways (1+1+1+1, 2+1+1, 2+2) and 5 has three. After the coin 5, the sum 5 gains the single-coin combination, giving 4.
Step by step
- Create
ways, anint[]of sizeamount + 1, so that indexamountexists. A newintarray is filled with zeros. - Set
ways[0]to 1. The sum 0 has exactly one combination, the empty one, and every other count grows from it. - Loop over the coins with
foreach (int coin in coins). This is the outer loop, which makes the counts combinations. - Loop over the sums from
coinup to and includingamount. A sum smaller than the coin can't contain it, and going upward allows repeated use. - Add
ways[a - coin]toways[a]with+=. This line is the whole dynamic programming idea: each entry reuses the counts for smaller sums. - Return
ways[amount]after the loops. Whenamountis 0 the inner loop never runs, and this returnsways[0], which is 1.
The solution in C#
class Solution {
public int Change(int amount, int[] coins) {
int[] ways = new int[amount + 1];
ways[0] = 1;
foreach (int coin in coins) {
for (int a = coin; a <= amount; a++) {
ways[a] += ways[a - coin];
}
}
return ways[amount];
}
}
The C# details worth knowing:
- Array defaults.
new int[amount + 1]gives zeros, so onlyways[0]needs setting. Arrays have a fixed length, which is why the size isamount + 1. foreachandfor.foreachreads each coin without an index. The inner loop needs a start and an end that depend oncoin, so it is a plainfor.intoverflow. Counts are 32-bit and wrap silently in C#'s default unchecked arithmetic. The lesson's constraints promise that every count fits, so the code doesn't guard against it.- Compound assignment.
ways[a] += ways[a - coin]reads and writes the same array in one statement. The indexa - coinis never negative because the loop starts ata = coin.
Complexity
Time: O(n · amount). Each of the n coins is handled by a loop of at most amount + 1 iterations, each doing one addition. For coins = {1, 2, 5} and amount = 5000 the method returns 1252001 almost instantly.
Space: O(amount). The only extra memory is ways, with amount + 1 integers.
Edge cases and common mistakes
- Amount 0. The answer is 1, from the empty combination. For
amount = 0andcoins = {7}the method returns 1. - A coin larger than the amount. The inner loop starts above
amountand doesn't run, so the coin is ignored.amount = 3withcoins = {2}returns 0. - A coin equal to the amount.
amount = 10withcoins = {10}returns 1. - Swapped loops. This counts orders and returns 7 instead of 4 for the third example. Coins must be outside.
- Looping downward. Going from
amountdown tocoinwould let each coin be used at most once, which solves a different problem. Here the direction must be upward. - Starting
ways[0]at 0. Every count would stay 0, since all counts grow from the empty combination. - Coin order. The result does not depend on the order of
coins:{5, 2, 1}with amount 5 also returns 4.
Testing
amount = 5,coins = {1, 2, 5}gives4.amount = 3,coins = {2}gives0.amount = 4,coins = {1, 2, 3}gives4.amount = 0,coins = {7}gives1.amount = 10,coins = {10}gives1.amount = 5000,coins = {1}gives1.
