LeetCode Meditations: Coin Change
Let's start with the description for this problem:
You are given an integer array
coins
representing coins of different denominations and an integeramount
representing a total amount of money.Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return
-1
.You may assume that you have an infinite number of each kind of coin.
For example:
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
Or:
Input: coins = [2], amount = 3
Output: -1
Input: coins = [2], amount = 3
Output: -1
Or:
Input: coins = [1], amount = 0
Output: 0
Input: coins = [1], amount = 0
Output: 0
Also, one of our constraints indicates that 1 <= coins.length <= 12
.
This problem is actually a familiar one, and you might've seen it in the context of a greedy problem. However, this version requires us to find the fewest number of coins, and a greedy approach wouldn't work.
Why that is true is neatly shown in a NeetCode video: for example, if our coins are [1, 3, 4, 5]
and the amount is 7
, the greedy approach will get 5
first, then it'll try all the maximum amounts until it has to be content with two 1
s, making it total 5
, 1
, and 1
. However, we can use fewer coins than that: 4
and 3
. So, let's see how we might go about doing that.
We have to build up to the amount
somehow, for the minimum number of coins we can use. For that, let's start with initializing an array:
let dp = Array.from({ length: amount + 1 }, () => amount + 1);
let dp = Array.from({ length: amount + 1 }, () => amount + 1);
We have this array of length amount + 1
as each index will hold the minimum number of coins we can use for each amount. For example, index 0 of our dp
array will hold the value of the minimum number of coins we can use for the amount of 0
; similarly, index 7 will hold the value of the minimum number of coins we can use for the amount of 7
.
We initialize each index with the placeholder value of amount + 1
, as the maximum number of coins can't exceed amount
(for example, the maximum number of coins we can use for 7
is 7: 1 + 1 + 1 + 1 + 1 + 1 + 1).
For the amount of 0
, the minimum number of coins we can use is obvious: 0
:
dp[0] = 0;
dp[0] = 0;
Then, we'll loop through this array, starting from index 1
, and for each index, we'll iterate through the coins:
for (let amountIdx = 1; amountIdx < dp.length; amountIdx++) {
for (const coin of coins) {
/* ... */
}
}
for (let amountIdx = 1; amountIdx < dp.length; amountIdx++) {
for (const coin of coins) {
/* ... */
}
}
If the coin we're looking at can be used for that amount (that is, amountIdx - coin >= 0
), then we'll update the value for that amount in our dp
array. It will be the minimum of either the value we already have, or 1 + dp[amountIdx - coin]
:
for (let amountIdx = 1; amountIdx < dp.length; amountIdx++) {
for (const coin of coins) {
if (amountIdx - coin >= 0) {
dp[amountIdx] = Math.min(dp[amountIdx], 1 + dp[amountIdx - coin]);
}
}
}
for (let amountIdx = 1; amountIdx < dp.length; amountIdx++) {
for (const coin of coins) {
if (amountIdx - coin >= 0) {
dp[amountIdx] = Math.min(dp[amountIdx], 1 + dp[amountIdx - coin]);
}
}
}
If, at the end, we can't match the total amount with any combination of coins, we have to return -1
.
The way to check for that is to check the condition where the last element equals amount + 1
. In that case, we can return -1
. Otherwise, we'll just return the last element which holds the minimum number of coins that make up the amount:
function coinChange(coins: number[], amount: number): number {
/* ... */
if (dp[dp.length - 1] === amount + 1) {
return -1;
}
return dp[dp.length - 1];
}
function coinChange(coins: number[], amount: number): number {
/* ... */
if (dp[dp.length - 1] === amount + 1) {
return -1;
}
return dp[dp.length - 1];
}
And, here is the final solution:
function coinChange(coins: number[], amount: number): number {
let dp = Array.from({ length: amount + 1 }, () => amount + 1);
dp[0] = 0;
for (let amountIdx = 1; amountIdx < dp.length; amountIdx++) {
for (const coin of coins) {
if (amountIdx - coin >= 0) {
dp[amountIdx] = Math.min(dp[amountIdx], 1 + dp[amountIdx - coin]);
}
}
}
if (dp[dp.length - 1] === amount + 1) {
return -1;
}
return dp[dp.length - 1];
}
function coinChange(coins: number[], amount: number): number {
let dp = Array.from({ length: amount + 1 }, () => amount + 1);
dp[0] = 0;
for (let amountIdx = 1; amountIdx < dp.length; amountIdx++) {
for (const coin of coins) {
if (amountIdx - coin >= 0) {
dp[amountIdx] = Math.min(dp[amountIdx], 1 + dp[amountIdx - coin]);
}
}
}
if (dp[dp.length - 1] === amount + 1) {
return -1;
}
return dp[dp.length - 1];
}
Time and space complexity
The time complexity is where is the amount + 1
and is the number of coins we have. We iterate through each value up to amount + 1
, and for each of those values, iterate again through each coin, doing a constant operation.
The space complexity depends on the amount
we're given as the size of our dp
array will grow as the amount increases, so we can say that it's where is the amount.
It's time for a deep breath. Even though we usually make peace with the solutions to dynamic programming problems, it's tough getting them in the first place — not only coming up with the solutions, but also understanding the already existing ones.
Next, we'll take a look at the problem Maximum Product Subarray. Until then, happy coding.