I'm going to retire a technical interview question that I've had ready to ask, but turns out to probably be too difficult. It's more a wizard-level interview question, or a casual technical discussion with peers who don't feel like they're under the gun.
How many ways are there to break a U.S. dollar into coins?
It seemed appropriate to ask this to some candidates, because once you know what you're trying to do, the algorithm in Python naturally reduces down to five or six lines of code.
def ways_to_break(amount, coins): this_coin = coins.pop(0) # If that was the last coin, only one way to break it. if not coins: return 1 # Sum the ways to break with the smaller coins. return sum([ways_to_break(amount - v, coins[:]) for v in range(0, amount+1, this_coin)]) print ways_to_break(100, [100, 50, 25, 10, 5, 1])
You could click on the gist with more comments and better variable names if you want to try to better understand that. It's not meant to be production code, it's a whiteboard snippet made runnable.
Here's what I hoped to look for, from the candidate: After maybe clarifying what I wanted, ("what about the silver dollar or half-dollar coins? Did you want permutations or combinations?"), the problem should seem well-defined, but hard. Then, I'd hope the candidate would break it down to the most trivial cases: "How many ways are there to break 5¢ if you have pennies and nickels? How about 10¢ with pennies, nickels and dimes?" Can these trivial cases be used to compose the bigger problem?
At that point, different areas of expertise would appear. Googlers might start jumping towards MapReduce, and already figure that Reduce is simply the summation function because the question is "how many", and have the thing worked out at scale.
Imperative programmers, having thought about the simplest cases, probably clue in that there's an iterative approach and a recursive approach.
Pythonistas who prefer functional programming probably have reduce(lambda x, y: x+y [x, y in ...]) ready to go.
That got me thinking. I didn't see the need for reduce and operator.add and enum. My solution above isn't above criticism, but I think it's easier to read than some of the alternatives. In truth, it really depends on who that reader is, and how their experience has conditioned them.
While writing the algorithm, I searched the question online, and was pleasantly surprised to see Raymond Hettinger mentioned for solving the puzzle. He's a distinguished Python core developer. Too bad he probably didn't have Python then, in 2001, to help him solve the problem in a scant few lines.
Taking it up a Notch
Adam Nevraumont proposed the following challenge:
Drop the penny, and try to break $1.01. The Canadian penny has been retired, after all.
He provided the answer: Check that the remaining coin can break the amount. Don't assume it can. This also relieves the restriction that the container of denominations be ordered!
def ways_to_break(amount, coins): this_coin = coins.pop() # If that was the only coin, can it can break amount? if not coins: if amount % this_coin: return 0 else: return 1 # Sum the ways to break with the other coins. return sum([ways_to_break(amount - v, coins[:]) for v in range(0, amount+1, this_coin)]) print ways_to_break(100, [1, 100, 10, 50, 25, 5])
(It's actually more efficient to pop the largest denominations first. But I wanted to show that you can use an unordered container now.)