ATM cash withdraw algorithm to distribute notes using $20 and $50 notes only

Question:

I want to begin by acknowledging that I know there are a ton of similar questions in SO and other websites, but all proposed solutions seem to have the same problem for my specific example.

Only using $20 and $50 notes, I’d like to calculate the less amount of notes that add up to the desired amount.

Although my question is language-agnostic, I’ll use Python for simplicity. I see a lot of people suggesting something like this:

def calculate_notes(amount, notes):
    remainder = amount
    results = {}

    for note in notes:
        n, remainder = divmod(remainder, note)
        results[note] = n

    return results

However, the method above returns the wrong result for many different scenarios, here are a couple:

print(calculate_notes(110, [50, 20]))  # Outputs {50: 2, 20: 0}, it should be {50: 1, 20: 3}
print(calculate_notes(130, [50, 20]))  # Outputs {50: 2, 20: 1}, it should be {50: 1, 20: 4}

I mean, I can make it work by adding a bunch of "if" statements, but I’m wondering if there’s a way to calculate it properly.

Invalid amounts like $10, $25 and $30 can be ignored.

Asked By: AndreFeijo

||

Answers:

Time Complexity- O(1) worst case for loop will run 5 times hence constant.

Space Complexity- O(1)

Code:

def calculate_notes(amount, notes):
    
    #Edge case amount=0
    if amount==0:
        return "Denomination not possible"
    options=[
        (0, {50: amount//50}),
        (10, {50: (amount//50)-1, 20:3}),
        (20, {50: amount//50, 20:1}),
        (30, {50: (amount//50)-1, 20:4}),
        (40, {50: amount//50, 20:2})
    ]
    for remainder,result in options:
        if amount%50==remainder and result[50]>=0: #result[50]>=0 for the cases like {amount: 10,30}
            return result
    return "Denomination not possible"


print(calculate_notes(110, [50, 20]))  
print(calculate_notes(20, [50, 20]))   
print(calculate_notes(10, [50, 20]))   

Credit to @user3386109. Instead of creating options a nested_list creating a dictionary.

Code:

def calculate_notes(amount, notes):
    #Edge case amount=0
    if amount==0:
        return "Denomination not possible"
    options={
        0: {50: amount//50},
        10: {50: (amount//50)-1, 20:3},
        20: {50: amount//50, 20:1},
        30: {50: (amount//50)-1, 20:4},
        40: {50: amount//50, 20:2}
    }
    remainder=amount%50
    if remainder in options.keys() and options[remainder][50]>=0:
        return options[remainder]
    return "Denomination not possible"

Output: [Both codes]

{50: 1, 20: 3}
{50: 0, 20: 1}
Denomination not possible
Answered By: Yash Mehta

I came up with the following implementation based on @user3386109 suggestion.

def calculate_notes(amount):
    notes_fifty, remainder = divmod(amount, 50)
    notes_twenty = 0

    if remainder != 0:
        if remainder % 20 != 0:
            notes_fifty -= 1
            remainder += 50

        notes_twenty = int(remainder / 20)

    return {'50': notes_fifty, '20': notes_twenty}

# Test with amounts between 40 and 200
for amount in range(40, 210, 10):
    notes = calculate_notes(amount)
    print(f'Amount {amount}', notes)
    assert (notes['50'] * 50) + (notes['20'] * 20) == amount

Output

Amount 40 {'50': 0, '20': 2}
Amount 50 {'50': 1, '20': 0}
Amount 60 {'50': 0, '20': 3}
Amount 70 {'50': 1, '20': 1}
Amount 80 {'50': 0, '20': 4}
Amount 90 {'50': 1, '20': 2}
Amount 100 {'50': 2, '20': 0}
Amount 110 {'50': 1, '20': 3}
Amount 120 {'50': 2, '20': 1}
Amount 130 {'50': 1, '20': 4}
Amount 140 {'50': 2, '20': 2}
Amount 150 {'50': 3, '20': 0}
Amount 160 {'50': 2, '20': 3}
Amount 170 {'50': 3, '20': 1}
Amount 180 {'50': 2, '20': 4}
Amount 190 {'50': 3, '20': 2}
Amount 200 {'50': 4, '20': 0}
Answered By: AndreFeijo

Please, note that 5 * 20 = 2 * 50 that’s why we can have at most 4 notes of 20 in the best solution (since we can change 5 notes of 20 into just 2 notes of 50). Let’s check all (0, 1, 2, 3, 4) possibilities and return (-1, -1) when exchange is not possible:

def calculate_notes(cash):
    if cash < 0:
        return (-1, -1)
    
    for twenty in range(5):
        if ((cash - 20 * twenty) % 50 == 0):
            return (twenty, (cash - 20 * twenty) // 50)
            
    return (-1, -1)

Here we have O(1) time and space complexity: for every cash we should check 5 cases in the worst case.

Demo:

print(calculate_notes(20))
print(calculate_notes(100))
print(calculate_notes(120))
print(calculate_notes(123))

output:

(1, 0)     # one 20, no 50
(0, 2)     # no 20, two 50s
(1, 2)     # one 20, two 50s
(-1, -1)   # not possible
Answered By: Dmitry Bychenko
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.