The ATM problem is a popular problem in FAANG interviews. The problem requires you to find the minimum number of banknotes needed to withdraw a specified amount of money from an ATM containing banknotes of various denominations.
In the previous article, we solved this problem using a greedy algorithm. However, this approach does not always guarantee an optimal solution.
In this article, we will solve the problem using dynamic programming.
Given an ATM containing N
banknotes of different denominations, write a function that will withdraw the specified sum of money using the minimum possible number of banknotes. The function should return an array of numbers
or null
if the sum cannot be withdrawn.
Dynamic programming is an approach to problem-solving that involves breaking down a problem into smaller subproblems. Typically, these subproblems differ only in their input values.
A classic example is the Fibonacci sequence. Before calculating F(N)
, we calculate F(N-1)
, and so on, until we reach the subproblem F(0)
, for which the answer is 1.
When solving problems using dynamic programming, we usually store the result of each subproblem in a table.
Let's say we have the following banknotes in an ATM: [1000, 500, 100]
, and we want to withdraw 1100
. We can create a table to store the values of F(N)
, where N
is the number of banknotes:
F(0)
We can obtain only the sum of 0 using 0 banknotes:
Sum |
Banknotes |
---|---|
0 |
[] |
F(1)
We add a banknote of 1000
and get 2 possible combinations:
Sum |
Banknotes |
---|---|
0 |
[] |
1000 |
[1000] |
F(2)
We add another banknote of 500
to the previous table and get 2 new combinations:
Sum |
Banknotes |
---|---|
0 |
[] |
1000 |
[1000] |
500 |
[500] |
1500 |
[1000, 500] |
F(3)
We add another banknote of 100
to the previous table and get 4 new combinations:
Sum |
Banknotes |
---|---|
0 |
[] |
1000 |
[1000] |
500 |
[500] |
1500 |
[1000, 500] |
100 |
[100] |
1100 |
[1000, 100] |
600 |
[500, 100] |
1600 |
[1000, 500, 100] |
Result
We look for the key 1100
in the table and get the answer [1000, 100]
.
Note that if we had obtained the same sum using different banknotes at any point in the process, we would store the result that used the fewest banknotes in the table. In our case, we consider the banknotes in decreasing order, so we prioritize the value already in the table. Thus, we obtain an optimal solution.
Create a table to store the sum and banknotes
Loop from 1 to N
, where N
is the number of banknotes, and for each n
, do the following:
Search the table for the row with the required sum and output the answer. If there is no such row in the table, then the solution for these values is not available.
const withdraw = (total, _coins) => {
coins = [..._coins].sort((a, b) => b - a);
// Main table
// key - sum
// value - array of banknotes
let sums = {0: []};
// Iterator from 1 to N banknotes
for (key in coins) {
const val = coins[key]; // Banknote value
const newSums = {}; // Temporary table
// Iterator over elements of the main table
for ([key, values] of Object.entries(sums)) {
const newKey = parseInt(key) + val; // New sum
newSums[newKey] = [...values, val]; // New set of banknotes
}
sums = {
...newSums,
...sums, // priority to the main table
};
}
return sums[total] || null;
};
To solve this problem, we iterate over N
banknotes, and for each banknote, we iterate over S
elements in the main table. The maximum possible number of rows in the table is equal to the withdrawal amount, considering that we don't store values more than the required value to withdraw.
Therefore, the time complexity of the algorithm is O(NS)
, where N
is the number of banknotes, and S
is the withdrawal amount.
We learned how to use dynamic programming to solve this problem. Unlike greedy algorithms, dynamic programming can guarantee an optimal solution to the ATM problem in all cases. The time complexity of dynamic programming is O(NS)
, where N
is the number of banknotes and S
is the withdrawal amount. Although this complexity is relatively high, it is still better than generating all possible combinations.