Solution: Coin Change
Let's solve the Coin Change problem using the Dynamic Programming pattern.
Statement
Given an integer total
that represents the target amount of money and a list of integers coins
that represents different coin denominations, find the minimum number of coins required to make up the total amount. If it’s impossible to achieve the target amount using the given coins, return -1
. If the target amount is 0, return 0
.
Note: You can assume that we have an infinite number of each kind of coin.
Constraints:
-
coins.length
-
coins[i]
-
total
Solution
So far, you’ve probably brainstormed some approaches and have an idea of how to solve this problem. Let’s explore some of these approaches and figure out which one to follow based on considerations such as time complexity and any implementation constraints.
Naive approach
The naive approach is to generates all possible combinations of given denominations such that in each combination, the sum of coins is equal to total
. From these combinations, choose the one with the minimum number of coins and return the minimum number required. If the sum of any combinations is not equal to total
then print -1.
In the worst case, the time complexity increases exponentially with the total
amount, which results in an algorithm edging toward total
. We can't improve the space complexity, but we can definitely improve our time complexity if we use dynamic programming to solve this problem.
Optimized solution using dynamic programming pattern
If we look at the problem, we might immediately think that it could be solved through a greedy approach. However, if we look at it closely, we’ll know that it’s not the correct approach here. Let’s take a look at an example to understand why this problem can’t be solved with a greedy approach.
Let's suppose we have coins = [1, 3, 4, 5]
and we want to find the total = 7
and we try to solve the problem with a greedy approach. In a greedy approach, we always start from the very end of a sorted array and traverse backward to find our solution because that allows us to solve the problem without traversing the whole array. However, in this situation, we start off with a 5
and add that to our total
. We then check if it’s possible to get a 7
with the help of either 4
or 3
, but as expected, that won't be the case, and we would need to add 1
twice to get our required total
.
The problem seems to be solved, and we have concluded that we need maximum 3
coins to get to the total
of 7
. However, if we take a look at our array, that isn’t the case. In fact, we could have reached the total
of 7
with just 2
coins: 4
and 3
. So, the problem needs to be broken down into subproblems, and an optimal solution can be reached from the optimal solutions of its subproblems.
To split the problem into subproblems, let's assume we know the number of coins required for some total
value and the last coin denomination is C. Because of the optimal substructure property, the following equation will be true:
But, we don't know what is the value of C yet, so we compute it for each element of the coins
array and select the minimum from among them. This creates the following recurrence relation:
Note: The problem can also be solved with the help of a simple recursive tree without any backtracking, but that would take extra memory and time complexity, as we can see in the illustration below.
Note: In the following section, we will gradually build the solution. Alternatively, you can skip straight to just the code.
Step-by-step solution construction
The idea is to solve the problem using the
We start our solution by creating a helper function that assists us in calculating the number of coins we need. It has three base cases to cover about what to return if the remaining amount is:
Less than zero
Equal to zero
Neither less than zero nor equal to zero
class CoinChange {public static int calculateMinimumCoins(int [] coins, int remainingAmount, int [] counter){if(remainingAmount < 0)return -1;if(remainingAmount == 0)return 0;if(counter[remainingAmount - 1] != -1)return counter[remainingAmount - 1];int minimum = -1;return 0;}public static int coinChange(int [] coins, int total){if(total < 1)return 0;System.out.println("The current total is more than zero, hence we need to call the helper function to calculate the minimum number of coins needed.\n");System.out.println("We will pass three parameters to our helper function:\n");System.out.println("The first parameter would be the coins we have which are: " + Arrays.toString(coins));System.out.println("The second parameter would be the total we have to calculate which is: " + total);System.out.print("The third parameter is the counter which is initially set to:");int[] l = new int[total];Arrays.fill(l, -1);System.out.println(Arrays.toString(l));return calculateMinimumCoins(coins, total, l);}public static void main( String args[] ) {int [] coins = {1, 3, 4, 5};int total = 7;coinChange(coins, total);}}
In the last case, when the remaining amount is neither of the base cases, we traverse the coins
array, and at each element, we recursively call the calculateMinimumCoins()
function, passing the updated remaining amount remainingAmount
minus the value of the current coin. This step effectively evaluates the number of coins needed for each possible denomination to make up the remaining amount. We store the return value of the base cases for each subproblem in a variable named result
. We then add 1
to the result
variable indicating that we're using this coin denomination in the process of making up the corresponding total
. Now, we assign this value to minimum
, which is initially set to infinity at the start of each path.
To avoid recalculating the minimum values for subproblems, we utilize the counter
array, which serves as a memoization table. This array stores the minimum number of coins required to make up each specific amount of money up to the given total. At the end of each path traversal, we update the corresponding index of the counter
array with the calculated minimum value. Finally, we return the minimum number of coins needed for the given total amount.
Let’s look at the slides below to understand the solution better.
Let’s implement the algorithm as discussed above:
class CoinChange {public static int calculateMinimumCoins(int [] coins, int remainingAmount, int [] counter){int result = 0;if(remainingAmount < 0)return -1;if(remainingAmount == 0)return 0;if(counter[remainingAmount - 1] != Integer.MAX_VALUE)return counter[remainingAmount - 1];int minimum = Integer.MAX_VALUE;for (int j = 0; j < coins.length; j++){result = calculateMinimumCoins(coins, remainingAmount - coins[j], counter);if(result >= 0 && result < minimum)minimum = 1 + result;}if(minimum != Integer.MAX_VALUE)counter[remainingAmount - 1] = minimum;elsecounter[remainingAmount - 1] = -1;return counter[remainingAmount -1];}public static int coinChange(int [] coins, int total) // main function{if(total < 1)return 0;int[] l = new int[total];Arrays.fill(l, Integer.MAX_VALUE);return calculateMinimumCoins(coins, total, l);}public static void main( String args[] ) {int [][] coins = {{2, 3, 4, 5},{1, 4, 6, 9},{6, 7, 8},{1, 2, 3, 4, 5},{14, 15, 18, 20}};int [] total = {7, 11, 27, 41, 52};for (int i = 0; i < total.length; i++){System.out.println(i + 1 + ".\tThe minimum number of coins required to find " + total[i] + " from " + Arrays.toString(coins[i]) + " is: "+ coinChange(coins[i], total[i]));System.out.println(PrintHyphens.repeat("-", 100));}}}
Just the code
Here’s the complete solution to this problem:
class CoinChange {public static int calculateMinimumCoins(int [] coins, int remainingAmount, int [] counter){int result = 0;if(remainingAmount < 0)return -1;if(remainingAmount == 0)return 0;if(counter[remainingAmount - 1] != Integer.MAX_VALUE)return counter[remainingAmount - 1];int minimum = Integer.MAX_VALUE;for (int j = 0; j < coins.length; j++){result = calculateMinimumCoins(coins, remainingAmount - coins[j], counter);if(result >= 0 && result < minimum)minimum = 1 + result;}if(minimum != Integer.MAX_VALUE)counter[remainingAmount - 1] = minimum;elsecounter[remainingAmount - 1] = -1;return counter[remainingAmount -1];}public static int coinChange(int [] coins, int total){if(total < 1)return 0;int [] l = new int[total];Arrays.fill(l, Integer.MAX_VALUE);return calculateMinimumCoins(coins, total, l);}// Driver codepublic static void main( String args[] ) {int [][] coins = {{2, 3, 4, 5},{1, 4, 6, 9},{6, 7, 8},{1, 2, 3, 4, 5},{14, 15, 18, 20}};int [] total = {7, 11, 27, 41, 52};for (int i = 0; i < total.length; i++){System.out.println(i + 1 + ".\tThe minimum number of coins required to find " + total[i] + " from " + Arrays.toString(coins[i]) + " is: "+ coinChange(coins[i], total[i]));System.out.println(PrintHyphens.repeat("-", 100));}}}
Solution summary
To recap, the solution to this problem can be divided into the following parts:
We first check the base cases, if
total
is eitheror less than : means no new coins need to be added because we have reached a viable solution. Less than
means our path can’t lead to this solution, so we need to backtrack.
After this, we use the top-down approach and traverse the given coin denominations.
At each iteration, we either pick a coin or we don’t.
If we pick a coin, we move on to solve a new subproblem based on the reduced
total
value.If we don’t pick a coin, then we look up the answer from the
counter
array if it is already computed to avoid recalculation.
Finally, we return the minimum number of coins required for the given total.
Time complexity
The time complexity for the above algorithm is
Space complexity
The space complexity for this algorithm is counter
array which is the size of total
.
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.