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
$300with available bills: 2×$50, 1×$100, 1×$200, the ATM will use 1×$200+ 1×$100(larger bills first) - If withdrawing
$600with available bills: 3×$200, 1×$500, the ATM will try to use the$500first, leaving$100needed. Since it cannot make exactly$100with 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 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) 631class 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 */ 691class 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 */ 641// 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} 53Solution 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
$20bills, index 1 represents$50bills, 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] * 5to 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
$500down 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 ansOnly 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
$500bills (index 4):- Can use:
min(600 // 500, 1) = min(1, 1) = 1bill - Take 1×
$500:ans[4] = 1 - Remaining:
amount = 600 - 500 = 100
- Can use:
-
Try
$200bills (index 3):- Can use:
min(100 // 200, 2) = min(0, 2) = 0bills - Take 0×
$200:ans[3] = 0 - Remaining:
amount = 100
- Can use:
-
Try
$100bills (index 2):- Can use:
min(100 // 100, 1) = min(1, 1) = 1bill - Take 1×
$100:ans[2] = 1 - Remaining:
amount = 100 - 100 = 0
- Can use:
-
Try
$50and$20bills:amount = 0, so we take 0 of each -
Check:
amount = 0✓ (exact change possible) -
Update inventory:
cnt = [0, 0, 0, 2, 0](removed 1×$100and 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
$500bills (index 4):- Can use:
min(600 // 500, 0) = min(1, 0) = 0bills (we have none!) - Remaining:
amount = 600
- Can use:
-
Try
$200bills (index 3):- Can use:
min(600 // 200, 2) = min(3, 2) = 2bills - Take 2×
$200:ans[3] = 2 - Remaining:
amount = 600 - 400 = 200
- Can use:
-
Try
$100bills (index 2):- Can use:
min(200 // 100, 0) = min(2, 0) = 0bills (we have none!) - Remaining:
amount = 200
- Can use:
-
Try
$50and$20bills: We have 0 of each, soamountstays 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
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.dwith 5 denominations andself.cntwith 5 counters - The
withdraw()method creates a temporary arrayansof 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 data structure is used to implement priority queue?
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!