2241. Design an ATM Machine
Problem Description
This problem asks you to implement an ATM machine system that handles deposits and withdrawals of banknotes.
The ATM works with 5 denominations of bills: $20
, $50
, $100
, $200
, and $500
. Initially, the ATM starts empty with no bills.
You need to implement a class ATM
with the following operations:
-
ATM()
: Constructor that initializes an empty ATM machine. -
deposit(banknotesCount)
: Takes an array of 5 integers representing the count of bills to deposit. The array follows the order[$20 bills, $50 bills, $100 bills, $200 bills, $500 bills]
. This operation adds these bills to the ATM's storage. -
withdraw(amount)
: Attempts to withdraw the specified dollar amount from the ATM. The withdrawal must follow a greedy approach using larger denominations first.- Returns an array of 5 integers showing how many of each bill denomination was withdrawn (in the same order as deposit:
$20
,$50
,$100
,$200
,$500
) - If the exact amount cannot be withdrawn using available bills, returns
[-1]
and no bills are withdrawn
- Returns an array of 5 integers showing how many of each bill denomination was withdrawn (in the same order as deposit:
Key withdrawal rule: The ATM must prioritize larger bills first and cannot backtrack. For example:
- If withdrawing
$300
with available bills: 2Γ$50
, 1Γ$100
, 1Γ$200
, the ATM will use 1Γ$200
+ 1Γ$100
(larger bills first) - If withdrawing
$600
with available bills: 3Γ$200
, 1Γ$500
, the ATM will try to use the$500
first, leaving$100
needed. Since it cannot make exactly$100
with remaining bills, the withdrawal fails and returns[-1]
The solution uses arrays to track denominations (d = [20, 50, 100, 200, 500]
) and their counts (cnt
). During withdrawal, it iterates from largest to smallest denomination, greedily taking as many bills as possible while not exceeding the remaining amount. If the exact amount cannot be achieved, it returns [-1]
; otherwise, it updates the bill counts and returns the withdrawal distribution.
Intuition
The key insight is recognizing that this is a greedy algorithm problem with a constraint - we must use larger denominations first and cannot backtrack once we've chosen a denomination.
Think about how a real ATM works: it tries to give you the fewest number of bills possible by using larger denominations first. This naturally leads to a greedy approach where we iterate through denominations from largest to smallest.
For each denomination during withdrawal, we ask: "How many bills of this denomination can I use without exceeding the remaining amount?" This is simply min(amount // denomination, available_bills)
. We take the minimum because we're limited by either:
- How many bills would perfectly divide into the remaining amount (
amount // denomination
) - How many bills we actually have available (
available_bills
)
The critical part is understanding why we need to check if the withdrawal is possible after attempting the greedy allocation. Since we can't backtrack, if we end up with a remaining amount > 0 after trying all denominations, it means our greedy choice made it impossible to give exact change. For example, if we need $600
and greedily take a $500
bill first, we're left needing $100
, but if we only have $200
bills remaining, we're stuck.
This "all or nothing" nature of the withdrawal (where we either complete the entire transaction or reject it) means we need to:
- First simulate the withdrawal to see if it's possible
- Only update our bill counts if the withdrawal succeeds
- Return
[-1]
if we can't make exact change
The deposit operation is straightforward - we're just adding to our inventory of bills, so we simply increment the counts for each denomination.
Learn more about Greedy patterns.
Solution Approach
The implementation uses simulation with a greedy algorithm to handle ATM operations.
Data Structure Setup:
self.d = [20, 50, 100, 200, 500]
: Array storing the denomination values in ascending orderself.cnt = [0] * 5
: Array tracking the count of each denomination (initially all zeros)- The indices in both arrays correspond to each other - index 0 represents
$20
bills, index 1 represents$50
bills, and so on
Deposit Operation:
The deposit
method is straightforward - we iterate through the input array banknotesCount
and add each count to our internal storage:
for i, x in enumerate(banknotesCount):
self.cnt[i] += x
This simply increments our bill inventory for each denomination. Time complexity is O(1)
since we always process exactly 5 denominations.
Withdraw Operation:
The withdraw
method implements the greedy approach with the following steps:
-
Initialize result array:
ans = [0] * 5
to track how many of each bill we'll withdraw -
Greedy allocation from largest to smallest:
for i in reversed(range(self.m)): ans[i] = min(amount // self.d[i], self.cnt[i]) amount -= ans[i] * self.d[i]
- We iterate backwards through denominations (from
$500
down to$20
) - For each denomination, we calculate the maximum bills we can use:
min(amount // self.d[i], self.cnt[i])
- This takes the minimum of: bills needed to not exceed amount vs. bills available
- We subtract the value of used bills from the remaining amount
- We iterate backwards through denominations (from
-
Validation check:
if amount > 0: return [-1]
If there's still amount left after trying all denominations, the withdrawal is impossible
-
Commit the transaction:
for i, x in enumerate(ans): self.cnt[i] -= x return ans
Only if the withdrawal is valid do we actually deduct the bills from our inventory and return the withdrawal distribution
The beauty of this approach is its simplicity - by processing denominations in descending order and greedily taking as many bills as possible at each step, we naturally prioritize larger bills while ensuring we don't exceed the requested amount. The final validation step ensures we only complete transactions that can provide exact change.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the ATM handles deposits and withdrawals.
Initial State:
- ATM is empty:
cnt = [0, 0, 0, 0, 0]
(representing counts of[$20, $50, $100, $200, $500]
)
Step 1: Deposit Operation
deposit([0, 0, 1, 2, 1])
- We're depositing: 0Γ
$20
, 0Γ$50
, 1Γ$100
, 2Γ$200
, 1Γ$500
- After deposit:
cnt = [0, 0, 1, 2, 1]
- Total cash in ATM:
$100 + $400 + $500 = $1000
Step 2: Successful Withdrawal
withdraw(600)
Let's trace through the greedy algorithm:
-
Start with
amount = 600
,ans = [0, 0, 0, 0, 0]
-
Try
$500
bills (index 4):- Can use:
min(600 // 500, 1) = min(1, 1) = 1
bill - Take 1Γ
$500
:ans[4] = 1
- Remaining:
amount = 600 - 500 = 100
- Can use:
-
Try
$200
bills (index 3):- Can use:
min(100 // 200, 2) = min(0, 2) = 0
bills - Take 0Γ
$200
:ans[3] = 0
- Remaining:
amount = 100
- Can use:
-
Try
$100
bills (index 2):- Can use:
min(100 // 100, 1) = min(1, 1) = 1
bill - Take 1Γ
$100
:ans[2] = 1
- Remaining:
amount = 100 - 100 = 0
- Can use:
-
Try
$50
and$20
bills:amount = 0
, so we take 0 of each -
Check:
amount = 0
β (exact change possible) -
Update inventory:
cnt = [0, 0, 0, 2, 0]
(removed 1Γ$100
and 1Γ$500
) -
Return:
[0, 0, 1, 0, 1]
Step 3: Failed Withdrawal
withdraw(600)
Now let's try to withdraw $600
again with our updated inventory:
-
Start with
amount = 600
,ans = [0, 0, 0, 0, 0]
-
Try
$500
bills (index 4):- Can use:
min(600 // 500, 0) = min(1, 0) = 0
bills (we have none!) - Remaining:
amount = 600
- Can use:
-
Try
$200
bills (index 3):- Can use:
min(600 // 200, 2) = min(3, 2) = 2
bills - Take 2Γ
$200
:ans[3] = 2
- Remaining:
amount = 600 - 400 = 200
- Can use:
-
Try
$100
bills (index 2):- Can use:
min(200 // 100, 0) = min(2, 0) = 0
bills (we have none!) - Remaining:
amount = 200
- Can use:
-
Try
$50
and$20
bills: We have 0 of each, soamount
stays at 200 -
Check:
amount = 200 > 0
β (cannot make exact change!) -
Return:
[-1]
(withdrawal failed, inventory unchanged)
This example demonstrates two key aspects:
- The greedy algorithm always tries larger bills first
- If exact change cannot be made, the entire withdrawal is rejected and no bills are dispensed
Solution Implementation
1from typing import List
2
3
4class ATM:
5 def __init__(self):
6 """Initialize ATM with supported denominations and their counts."""
7 # Supported banknote denominations in ascending order: $20, $50, $100, $200, $500
8 self.denominations = [20, 50, 100, 200, 500]
9 self.num_denominations = len(self.denominations)
10 # Current count of each denomination in the ATM
11 self.banknote_counts = [0] * self.num_denominations
12
13 def deposit(self, banknotesCount: List[int]) -> None:
14 """
15 Deposit banknotes into the ATM.
16
17 Args:
18 banknotesCount: List of 5 integers representing count of each denomination
19 [count_20, count_50, count_100, count_200, count_500]
20 """
21 for i, count in enumerate(banknotesCount):
22 self.banknote_counts[i] += count
23
24 def withdraw(self, amount: int) -> List[int]:
25 """
26 Withdraw the specified amount from the ATM using a greedy approach.
27 Uses largest denominations first to minimize number of banknotes.
28
29 Args:
30 amount: The amount to withdraw
31
32 Returns:
33 List of 5 integers representing count of each denomination withdrawn,
34 or [-1] if the exact amount cannot be withdrawn
35 """
36 # Track how many of each denomination to withdraw
37 withdrawal_counts = [0] * self.num_denominations
38
39 # Try to use largest denominations first (greedy approach)
40 for i in reversed(range(self.num_denominations)):
41 # Calculate maximum notes of this denomination we can use
42 # Limited by both the amount needed and available notes
43 notes_to_use = min(amount // self.denominations[i], self.banknote_counts[i])
44 withdrawal_counts[i] = notes_to_use
45 # Reduce remaining amount
46 amount -= notes_to_use * self.denominations[i]
47
48 # Check if we could withdraw the exact amount
49 if amount > 0:
50 return [-1]
51
52 # Update the ATM's banknote counts after successful withdrawal
53 for i, count in enumerate(withdrawal_counts):
54 self.banknote_counts[i] -= count
55
56 return withdrawal_counts
57
58
59# Your ATM object will be instantiated and called as such:
60# obj = ATM()
61# obj.deposit(banknotesCount)
62# param_2 = obj.withdraw(amount)
63
1class ATM {
2 // Array of denomination values in ascending order: $20, $50, $100, $200, $500
3 private int[] denominations = {20, 50, 100, 200, 500};
4
5 // Number of denomination types
6 private int numDenominations = denominations.length;
7
8 // Array to store the count of each denomination currently in the ATM
9 // Index 0: $20 bills, Index 1: $50 bills, etc.
10 private long[] banknoteCounts = new long[5];
11
12 /**
13 * Constructor initializes an empty ATM
14 */
15 public ATM() {
16 }
17
18 /**
19 * Deposits banknotes into the ATM
20 * @param banknotesCount Array where banknotesCount[i] is the number of bills
21 * of denomination[i] to deposit
22 */
23 public void deposit(int[] banknotesCount) {
24 // Add each denomination count to the existing counts in the ATM
25 for (int i = 0; i < banknotesCount.length; i++) {
26 banknoteCounts[i] += banknotesCount[i];
27 }
28 }
29
30 /**
31 * Attempts to withdraw the specified amount from the ATM
32 * @param amount The total amount to withdraw
33 * @return Array of banknote counts used for withdrawal, or [-1] if withdrawal is impossible
34 */
35 public int[] withdraw(int amount) {
36 // Array to store the number of each denomination to withdraw
37 int[] withdrawalCounts = new int[numDenominations];
38
39 // Greedy approach: Start with largest denominations first
40 for (int i = numDenominations - 1; i >= 0; i--) {
41 // Calculate maximum bills of current denomination we can use
42 // Limited by either the amount needed or available bills
43 withdrawalCounts[i] = (int) Math.min(amount / denominations[i], banknoteCounts[i]);
44
45 // Reduce the remaining amount by the value of bills used
46 amount -= withdrawalCounts[i] * denominations[i];
47 }
48
49 // If there's still amount left, withdrawal is impossible
50 if (amount > 0) {
51 return new int[] {-1};
52 }
53
54 // Withdrawal is possible, so deduct the bills from ATM inventory
55 for (int i = 0; i < numDenominations; i++) {
56 banknoteCounts[i] -= withdrawalCounts[i];
57 }
58
59 return withdrawalCounts;
60 }
61}
62
63/**
64 * Your ATM object will be instantiated and called as such:
65 * ATM obj = new ATM();
66 * obj.deposit(banknotesCount);
67 * int[] param_2 = obj.withdraw(amount);
68 */
69
1class ATM {
2public:
3 ATM() {
4 // Constructor initializes the ATM with zero banknotes
5 }
6
7 void deposit(vector<int> banknotesCount) {
8 // Add the deposited banknotes to the ATM's inventory
9 // banknotesCount[i] represents the number of banknotes for denomination[i]
10 for (int i = 0; i < banknotesCount.size(); ++i) {
11 banknoteCount[i] += banknotesCount[i];
12 }
13 }
14
15 vector<int> withdraw(int amount) {
16 // Attempt to withdraw the specified amount using available banknotes
17 // Use greedy approach: start with largest denominations first
18
19 vector<int> result(NUM_DENOMINATIONS);
20
21 // Iterate through denominations from largest to smallest
22 for (int i = NUM_DENOMINATIONS - 1; i >= 0; --i) {
23 // Calculate how many banknotes of this denomination we can use
24 // Limited by both the amount needed and available banknotes
25 result[i] = min(static_cast<long long>(amount / DENOMINATIONS[i]),
26 banknoteCount[i]);
27
28 // Reduce the remaining amount by the value of banknotes used
29 amount -= result[i] * DENOMINATIONS[i];
30 }
31
32 // Check if we successfully withdrew the exact amount
33 if (amount > 0) {
34 // Cannot make exact change, return failure indicator
35 return {-1};
36 }
37
38 // Update the ATM's banknote inventory after successful withdrawal
39 for (int i = 0; i < NUM_DENOMINATIONS; ++i) {
40 banknoteCount[i] -= result[i];
41 }
42
43 return result;
44 }
45
46private:
47 // Available banknote denominations in ascending order: $20, $50, $100, $200, $500
48 static constexpr int DENOMINATIONS[5] = {20, 50, 100, 200, 500};
49
50 // Number of different denomination types
51 static constexpr int NUM_DENOMINATIONS = sizeof(DENOMINATIONS) / sizeof(DENOMINATIONS[0]);
52
53 // Current count of each denomination in the ATM
54 // banknoteCount[i] stores the count of DENOMINATIONS[i] banknotes
55 long long banknoteCount[NUM_DENOMINATIONS] = {0};
56};
57
58/**
59 * Your ATM object will be instantiated and called as such:
60 * ATM* obj = new ATM();
61 * obj->deposit(banknotesCount);
62 * vector<int> param_2 = obj->withdraw(amount);
63 */
64
1// Denominations of banknotes in ascending order: $20, $50, $100, $200, $500
2const denominations: number[] = [20, 50, 100, 200, 500];
3const denominationCount = denominations.length;
4
5// Array to store the count of each denomination currently in the ATM
6let banknoteCounts: number[] = new Array(denominationCount).fill(0);
7
8/**
9 * Deposits banknotes into the ATM
10 * @param banknotesCount - Array where index i represents the count of denomination[i] bills to deposit
11 */
12function deposit(banknotesCount: number[]): void {
13 // Add the deposited banknotes to the existing counts
14 for (let i = 0; i < banknotesCount.length; i++) {
15 banknoteCounts[i] += banknotesCount[i];
16 }
17}
18
19/**
20 * Withdraws the specified amount from the ATM using available banknotes
21 * @param amount - The amount to withdraw
22 * @returns Array of banknote counts used for withdrawal, or [-1] if withdrawal is impossible
23 */
24function withdraw(amount: number): number[] {
25 // Initialize result array to track how many of each denomination to withdraw
26 const withdrawalCounts: number[] = new Array(denominationCount).fill(0);
27
28 // Greedy approach: Start from the largest denomination and work backwards
29 for (let i = denominationCount - 1; i >= 0; i--) {
30 // Calculate how many bills of this denomination we can use
31 // Limited by both the amount needed and available bills
32 const billsToUse = Math.min(
33 Math.floor(amount / denominations[i]),
34 banknoteCounts[i]
35 );
36
37 withdrawalCounts[i] = billsToUse;
38 amount -= billsToUse * denominations[i];
39 }
40
41 // Check if we couldn't fulfill the exact amount
42 if (amount > 0) {
43 return [-1];
44 }
45
46 // Deduct the withdrawn banknotes from the ATM's inventory
47 for (let i = 0; i < denominationCount; i++) {
48 banknoteCounts[i] -= withdrawalCounts[i];
49 }
50
51 return withdrawalCounts;
52}
53
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Initializes fixed-size arrays with 5 elementsdeposit()
:O(1)
- Iterates through a fixed array of size 5 to update countswithdraw()
:O(1)
- Performs two fixed iterations through arrays of size 5 (one reversed loop for calculation and one forward loop for updating counts)
Since all operations work with arrays of constant size (5 denominations: 20, 50, 100, 200, 500), the time complexity for all methods is O(1)
.
Space Complexity:
- The class maintains two fixed-size arrays:
self.d
with 5 denominations andself.cnt
with 5 counters - The
withdraw()
method creates a temporary arrayans
of size 5 - All space usage is constant regardless of input size
Therefore, the overall space complexity is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Handling Transaction Atomicity
The Problem: A critical mistake is modifying the ATM's banknote counts before verifying if the withdrawal is actually possible. Consider this flawed implementation:
def withdraw(self, amount: int) -> List[int]:
withdrawal_counts = [0] * self.num_denominations
for i in reversed(range(self.num_denominations)):
notes_to_use = min(amount // self.denominations[i], self.banknote_counts[i])
withdrawal_counts[i] = notes_to_use
# WRONG: Updating counts immediately
self.banknote_counts[i] -= notes_to_use
amount -= notes_to_use * self.denominations[i]
if amount > 0:
# Problem: We've already modified the ATM state!
return [-1]
return withdrawal_counts
This breaks the atomicity principle - if the withdrawal fails, the ATM state has already been corrupted and we cannot easily roll back the changes.
The Solution: Always validate the entire transaction before modifying any state. Only update the banknote counts after confirming the withdrawal is possible:
# First, simulate the withdrawal
for i in reversed(range(self.num_denominations)):
notes_to_use = min(amount // self.denominations[i], self.banknote_counts[i])
withdrawal_counts[i] = notes_to_use
amount -= notes_to_use * self.denominations[i]
# Then, check if it's valid
if amount > 0:
return [-1]
# Finally, commit the changes
for i, count in enumerate(withdrawal_counts):
self.banknote_counts[i] -= count
Pitfall 2: Incorrect Greedy Logic with Division
The Problem: Using floating-point division or incorrect integer division can lead to attempting to withdraw more bills than needed:
# WRONG: This might try to use more bills than necessary notes_to_use = self.banknote_counts[i] # Just taking all available bills if notes_to_use * self.denominations[i] <= amount: withdrawal_counts[i] = notes_to_use amount -= notes_to_use * self.denominations[i]
The Solution: Always use integer division to calculate the exact number of bills needed, then take the minimum with available bills:
# CORRECT: Calculate exact bills needed, then take minimum with available
notes_to_use = min(amount // self.denominations[i], self.banknote_counts[i])
Pitfall 3: Attempting to Optimize with Backtracking
The Problem: Some might think the greedy approach could fail in cases where a different combination would work, leading to complex backtracking implementations:
# UNNECESSARY: Trying to find alternative combinations
def withdraw_with_backtrack(self, amount, index=4):
if amount == 0:
return True
if index < 0 or amount < 0:
return False
# Try different combinations...
for count in range(self.banknote_counts[index], -1, -1):
if count * self.denominations[index] <= amount:
# Complex recursive logic...
Why This is Wrong: The problem specifically states that the ATM must use a greedy approach with larger denominations first. The ATM doesn't search for alternative combinations - it strictly follows the greedy algorithm. If the greedy approach fails, the withdrawal fails, even if another combination might work.
For example, if you need to withdraw 200 and 1Γ$500:
- The ATM will use 1Γ100 needed
- Even though 3Γ$200 would work perfectly, the ATM won't backtrack
- The withdrawal returns [-1]
The correct approach is to stick with the simple greedy algorithm as specified in the problem requirements.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!