Changemaking

It's time to talk ChangeMaker! You thought it was dead since you saw it in CMSI 186, but it's back, bigger than ever!

OK, maybe not that much hype, but it's certainly a problem that will be interesting to revisit.

Let's remind ourselves of the problem specification, then contextualize it to our recent discussions.


Problem Description


Disclaimer: this may seem like a toy problem (unless we plan on coding vending machines), but belongs to class of problems called Knapsack Problems that are employed everywhere, including: generations of keys in cryptography, maximizing investments in stocks, and finding the best set of loot to sell in video games given a limited inventory.

The ChangeMaking Problem is specified such that for a given set of coin denominations \(D\) and some sum of money to make change for \(N\), we must find the optimal (i.e., minimal) number of coins by which to make the requested amount.

Pictorially, this looks like:


Greedy Approach


For optimal change-making, we in the US are quite lucky, since the algorithm to accomplish it is very simple.

If we were to compute the change for \(0.81\) with the standard US currency, how would we go about this?

Simply take the largest denomination from the amount remaining, then recurse on the rest!

Consider the steps of finding the result of the changemaker above: the least coins to make \($0.81\).

This strategy works for US coin currency and is simple to implement!

The funny thing is that the strategy of taking the "best" action available at any state has an ironically relevant name:

Search strategies that always take the next best option available at any state are called greedy strategies.

The greedy strategy works out for US currency in change making... but is that true for all currencies?


Greed is (Sometimes) Bad


Consider a currency with denominations \(D = \{1, 3, 4\}\) and we wish to make change for \(6\) "cents".

With these denominations, what will the greedy approach return as the solution to the changemaking problem for \(6\) cents? Is this the optimal amount?

It will return 3 coins: \((4, 1, 1)\), but this is not optimal! We could have made \(6\) cents with 2 coins: \((3,3)\).

So, it would seem, the greedy method does not always return the optimal solution for changemaking, depending on the input denominations.

You might say that, sometimes, Greed is one of the 7 deadly algorith-sins... well, maybe you wouldn't say that, but I certainly would. And have. Just now.

So, here we have a problem wherein a greedy method is breaking down, and we need some way to examine possible combinations of coins / choices to see which is the best. What algorithmic paradigm might be useful here?

Let's try to think about it in terms of a search problem!



Searching for Change

No, this section title is not the album name of my band's next EP, nor is it related to what you might do when you hear something jingly in your couch cushions.

Rather, let's try to think about Changemaker as a search problem, and note some interesting properties of it.


Formalizing the Changemaker Problem


Recall that in Search formalizations, we must define 5 properties; what are they and how do we define them for the Changemaker Search Problem?

The Changemaker Search Problem can be defined as:

  • Initial State: amount of cents remaining to make change for.

  • Goal State: 0 cents left.

  • Actions: give a coin from amongst the denominations that are less than the amount remaining in the state.

  • Transitions: subtract that coin's value from the state.

  • Cost: uniform cost, 1 per coin given.

Neato! It's like what we've been learning is applicable to other problems or something!

How about we see it in action?


Draw the search tree of the Changemaking Problem; we will emphasize the nodes are actually a type of min-node for reasons that will be later apparent, or now-apparent if you're really sharp.

"Hey, wait a second, Andrew -- that looks almost exactly like that dumb Nim tree! You barely made up a new example at all!" I'm not afraid of change.

Seeing this full search tree, how is it different than the Maze Pathfinding search tree that we had been examining originally?

The actions along a path to a goal matter, but crucially, their order does not.

Believe it or not, this is a really big difference:

  • In Changemaking: taking \((1, 1, 4) = (1, 4, 1) = (4, 1, 1)\).

  • In Pathfinding: taking \((U, R, R) \ne (R, U, R) \ne (R, R, U)\) (there might be walls / mud tiles avoided in one path that isn't in another).

Why is this property significant? Does it pose any challenges we might have an idea how to solve?

It leads to *far* more repeated states and extraneous paths that might get explored! Intuitively, we would like to use memoization to reduce the amount of repeated work! However, since this is the Changemaker problem, I'll use the more pun-appropriate synonym for memoization: caching.

"Andrew, did you just choose this problem so that you could make as many money puns as possible?" That's a safe bet!

However, in order to use caching on this search tree, we need to know the solution to the subproblem that we're memoizing! What search strategy does this suggest we use?

Depth-first search so that we can reach terminal states more quickly, and therefore cache more sub-problem solutions that we are more likely to encounter in this setting.

Reflect: why would Breadth-first Search not scale well in problems like these, even though it appears to be a good strategy above?

Observe the left-most subtree / subproblem in our full search tree. In order to cache the state with \(2\) cents remaining, we must reach the terminal state 2 coins below it, which is why DFS is desirable here.

However, once we've cached that \(2\) state, observe how many other times we save ourselves from having to recompute it:


So, if you're like me when I first saw this approach to changemaking, you might ask: "Well what the HE-double-hockey-sticks, this is so much easier to understand than what we did in 186!" (I was very young and thus had not discovered any semblance of curse words).

While (in my honest opinion) viewing problems like these as search has a nice, intuitive binding, we should understand that application of memoization in this context is actually a special case of a broader programming paradigm called Dynamic Programming.

Let's take a closer look at that now!



Dynamic Programming

Dynamic Programming is another programming paradigm wherein the optimal solution to a large problem can be found from decomposing it and finding the optimal solution to those sub-problems.

This might sound a lot like divide-and-conquer like we've seen in the past, but there's actually another requirement for dynamic programming approaches:

Applying dynamic programming to a problem requires that the specific problem exhibit optimal substructure, meaning that the optimal solution to the problem can be constructed from optimal solutions to its subproblems.

Let's think about this optimal substructure requirement a bit more intuitively...


Optimal Substructure Intuition


Consider the case where we have our optimal set of coins \(S\) for the Changemaking problem wherein \(N = 6\) such that: $$changemaker(6) = S = \{3, 3\}$$

The optimal substructure property can be intuited this way: removing a coin \(c\) from \(S\) will provide the optimal solution to the problem $$changemaker(N) = S \Rightarrow changemaker(N - c) = S \setminus c$$

Testing that out: $$changemaker(6) = \{3, 3\} \Rightarrow changemaker(6 - 3) = \{3\}$$

Optimal substructure is money.

As it turns out, we've already been exploiting optimal substructure in our Maze Pathfinder! How so?

On top of the actual paths, consider how we computed the history cost of every node that we generated with A*:

$$g(n) = g(parent(n)) + cost(n)$$

A nice property of problems with optimal substructure is that they can be represented as a recurrence.

Consider the Fibonacci sequence in which the nth entry is the sum of the previous two entries: $$1, 1, 2, 3, 5, 8, 13, ...$$

How would you represent the solution to the nth Fibonacci number, \(F(n)\), as a recurrence?

\(F(n) = 1\) if \(n \le 1\), \(F(n) = F(n-1) + F(n-2)\) otherwise.

Turns out we've been seeing optimal substructure a lot! Imagine that!

In general, optimal substructure is proven by induction (glad you took Methods of Proof, yes?), so we'll see some convincing intuitions for that case in a bit.


Types of Dynamic Programming


Because of the optimal substructure requirement, there are two main approaches to dynamic programming:

Bottom-up Dynamic Programming [Tabulation][From leaves to root]: solves the smallest sub-problems first, then uses those solutions as the foundation on which to build up to larger and larger subproblems.

If we wanted to compute the 10th number in the Fibonacci sequence, i.e., \(Fib(10)\), the bottom-up approach would begin by solving: \(Fib(1), Fib(2), Fib(3), ...\)

This is reasonable because \(Fib(3) = Fib(2) + Fib(1)\), so having the earlier entries pre-computed means we have them immediately availabe as stepping-stones into the later entries.

This is the type of dynamic programming you saw in CMSI 186! We'll review it later to complete our juxtaposition.


Top-down Dynamic Programming [Memoization][From root to leaves]: start with the large problem, then recursively identify specific subproblems to solve.

In the Fibonacci example, if we wanted \(Fib(10)\) we would start by knowing that \(Fib(10) = Fib(9) + Fib(8)\), at which point we would recursively call the \(Fib\) function on each of the smaller subproblems, caching those that we had already computed along the way.

This is the type of dynamic programming that we related to the search formalization at the start of this lecture.


However, the formalization in terms of a search problem may need some tweaking for some reasons we need to discuss...

Let's take a holistic perspective on what distinguishes these two approaches, and when one might be preferable to the other.



Top-Down Dynamic Programming

Thus far, our conceptual / mental model of search in problems with optimal substructure has been to physically construct a tree because the nodes carry vital info about the paths that led up to them.

However, in some problems (like Changemaker), we see that the search tree doesn't add a whole lot of value.

What aspects of building the search tree (even with memoization) for the purpose of DFS might be wasteful, either computationally or in terms of tax on memory?

The nodes need to be dynamically allocated and deallocated as the search progresses, leading to a performance overhead (albeit no change in complexity).

Is there another, familiar programming paradigm we might use instead to mimic a DFS on the search tree, while maintaining the state that used to be stored in the nodes?

Yes! Just use recursion, and store any necessary state in the recursive function's parameters!

Note: we could *not* have done this with our classical search problems, like A*, because the frontier needs to maintain multiple avenues for exploration at time, and may have different *priorities* for each exploration order (which cannot easily be encoded recursively).

Still, for problems like Changemaker, it's nice to see a slightly more efficient implementation.


Mapping Search Trees to Recursion


Consider the following "ply" (i.e., level) in the search tree, and how the tree semantics can be translated into recursive calls to the changemaker:

Why do you think DFS on a search tree and recursion are so interchangeable as operations?

Because DFS uses a stack frontier, and recursion uses a call stack! They're both stackin' that cash!

somebody, send help, I can't stop.

The other nice benefit in the top-down DP approach: the call stack will automatically pop items after all subproblems have been solved, automatically saving space.

The downside: the stack only has so much room, and both calling / returning from functions has some computational overhead.


Whew! That's all we have room in our wallets for today; next time: we compare the top-down and (hopefully familiar) bottom-up approaches to see the best uses for each!



  PDF / Print