Facebook Pixel

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:

  1. ATM(): Constructor that initializes an empty ATM machine.

  2. 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.

  3. 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

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. How many bills would perfectly divide into the remaining amount (amount // denomination)
  2. 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:

  1. First simulate the withdrawal to see if it's possible
  2. Only update our bill counts if the withdrawal succeeds
  3. 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 order
  • self.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:

  1. Initialize result array: ans = [0] * 5 to track how many of each bill we'll withdraw

  2. 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
  3. Validation check:

    if amount > 0:
        return [-1]

    If there's still amount left after trying all denominations, the withdrawal is impossible

  4. 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 Evaluator

Example 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:

  1. Start with amount = 600, ans = [0, 0, 0, 0, 0]

  2. 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
  3. 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
  4. 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
  5. Try $50 and $20 bills: amount = 0, so we take 0 of each

  6. Check: amount = 0 βœ“ (exact change possible)

  7. Update inventory: cnt = [0, 0, 0, 2, 0] (removed 1Γ—$100 and 1Γ—$500)

  8. Return: [0, 0, 1, 0, 1]

Step 3: Failed Withdrawal

withdraw(600)

Now let's try to withdraw $600 again with our updated inventory:

  1. Start with amount = 600, ans = [0, 0, 0, 0, 0]

  2. Try $500 bills (index 4):

    • Can use: min(600 // 500, 0) = min(1, 0) = 0 bills (we have none!)
    • Remaining: amount = 600
  3. 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
  4. Try $100 bills (index 2):

    • Can use: min(200 // 100, 0) = min(2, 0) = 0 bills (we have none!)
    • Remaining: amount = 200
  5. Try $50 and $20 bills: We have 0 of each, so amount stays at 200

  6. Check: amount = 200 > 0 βœ— (cannot make exact change!)

  7. 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 elements
  • deposit(): O(1) - Iterates through a fixed array of size 5 to update counts
  • withdraw(): 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 and self.cnt with 5 counters
  • The withdraw() method creates a temporary array ans 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 600withavailablebills:3Γ—600 with available bills: 3Γ—200 and 1Γ—$500:

  • The ATM will use 1Γ—500first(greedy),leaving500 first (greedy), leaving 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More