Recursion

All Coin Change Ways

Backtracking

You're running a cash register and need to make change for a certain amount amount. You can only use certain denominations of coins, given in an array values. For example, you might use 3 dollar coins and 9 dollar coins to break up the amount.

Return all the possible configurations of coins you can use to break up the amount. You should return each configuration in increasing order.

Example:

amount = 7values = [2, 3, 6, 7]=> returns [[2, 2, 3], [7]]\notag \begin{align*} &\texttt{amount} \texttt{ = 7} \\ &\texttt{values} \texttt{ = } \texttt{[2, 3, 6, 7]} \\ &\texttt{=> returns [[2, 2, 3], [7]]} \end{align*}

Explanation: You can break the amount into 2+2+3=7. You can also break it into 7=7.

First Few Test Cases:
allCashRegisterOptions(7, [2, 3, 6, 7])
# [[2, 2, 3], [7]]
allCashRegisterOptions(12, [2, 3])
# [[2, 2, 2, 2, 2, 2], [2, 2, 2, 3, 3], [3, 3, 3, 3]]
allCashRegisterOptions(10, [1])
# [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]

0. Problem

We'll generate all the possible ways we can make change for the amount, using a recursive function. The function needs to know the current solution we have so far, currCoins. We'll also have it record the total sum of coins we have so far total so we don't have to waste time computing it when it's needed.

We also need to make sure we don't double count the same configuration of coins twice. The trick to doing this is to keep adding the same coin value over and over again, and once we move past it we never go back to it. We can do this by keeping track of which index in values we shoud use, called idx.

visitAll(currCoins, total, idx)

1. Recursion

We can generate solutions like this:

solns = []

visitAll(currCoins, total, idx) = (

    # add current coin's value again

    visitAll(currCoins + [values[idx]], total + values[idx], idx)

    # move on to the next coin

    visitAll(currCoins, total, idx+1)
)

2. Base case

When we get to the amount we want, we have a solution.

if total == amount:
    solns.append(currCoins) # add to our solutions
    return

We also want to stop if total goes over amount or if we go past the last possible index.

if total > amount or idx == len(values):
    return

3. Code

Here's the recursive code:

Open SpoilerClose Spoiler
def allCashRegisterOptions(values, amount):
    solns = []

    def visitAll(currCoins, total, idx):
        # base case
        if total > amount or idx == len(values):
            return
        if total == amount:
            solns.append(currCoins)
            return

        # recursion
        visitAll(currCoins + [values[idx]], total + values[idx], idx)

        visitAll(currCoins, total, idx+1)

    visitAll(0, [], 0)
    return solns

Time Complexity O(2⋅amount⋅vamount)O(2 \cdot \tt{amount} \cdot v^{\tt amount}), where v\tt v is the number of values. We need to build all vamount\texttt{v}^{\texttt{amount}} possibilities, and each has up to amount\texttt{amount} entries in it. There's really a factor of 2 here since currCoins + [values[idx]] is inefficient and recreates a whole new array each addition.

Space Complexity O(amount⋅vamount)O(\tt{amount} \cdot v^{\tt amount})

Call Stack Complexity O(amount2)O(\texttt{amount}^2). The recursion might go amount\tt amount calls deep. Each of those calls requires storing the currCoins\tt currCoins array, which contains up to amount\tt amount entries.

It's slow to do currCoins + [coins[idx]]\texttt{currCoins + [coins[idx]]} since it creates a whole new array. This is where Backtracking comes in. Backtracking is when we make currCoins global so we never create a new array. We'll also make idx and amount global to go full force with Backtracking (but this doesn't affect the time or space complexity).


def allCashRegisterOptions(amount, nums):
    # global parameters
    numbers = []
    idx = 0 
    total = 0

    solns = []


    def visitAll():
        # without `nonlocal` we'd just be setting local variables
        nonlocal numbers, idx, total, solns

        # base case
        if total > amount or idx == len(nums):
            return
        # recursion
        if total == amount:
            solns.append(numbers.copy())
            return

        numbers.append(nums[idx])
        total += nums[idx]
        visitAll()
        total -= nums[idx]
        numbers.pop()

        idx += 1
        visitAll()
        idx -= 1

    visitAll()
    return solns

Time Complexity O(amount⋅vamount)O(\tt{amount} \cdot \texttt{v}^{\tt amount}), where v\texttt{v} is the length of values. We need to build all vamount\texttt{v}^{\texttt{amount}} possibilities, and each has up to amount\texttt{amount} entries in it.

Space Complexity O(amount⋅vamount)O(\tt{amount} \cdot \texttt{v}^{\tt amount}) for the output array. The Call Stack and the global parameters also use up some space, but the output array's space complexity beats them by far.

Call Stack Complexity O(amount)O(\texttt{amount}). The Call Stack gets O(amount)O(\texttt{amount}) calls deep, and each call uses O(1)O(1) space.

Mark as Completed:
Submits:
allCashRegisterOptions
Test your code to get an output here!
allCashRegisterOptions(
)
Test your code to get an output here!