We use cookies to personalize content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners. By clicking "Accept Cookies," scrolling this page, clicking a link or continuing to browse otherwise, you agree to the use of cookies. Privacy and Cookie Policies
You only have 3 free questions left (including this one).
But it doesn't have to end here! Sign up for the 7-day coding interview crash course and you'll get a free Interview Cake problem every week.
You're in!
Your quirky boss collects rare, old coins...
They found out you're a programmer and asked you to solve something they've been wondering for a long time.
Write a method that, given:
an amount of money
an array of coin denominations
computes the number of ways to make the amount of money with coins of the available denominations.
Example: for amount=4 (4¢) and denominations=[1,2,3] (1¢, 2¢ and 3¢), your program would output 4—the number of ways to make 4¢ with those denominations:
1¢, 1¢, 1¢, 1¢
1¢, 1¢, 2¢
1¢, 3¢
2¢, 2¢
Gotchas
What if there's no way to make the amount with the denominations? Does your method have reasonable behavior?
We can do this in O(n∗m) time and O(n) space, where n is the amount of money and m is the number of denominations.
A simple recursive approach works, but you'll find that your method gets called more than once with the same inputs. We can do better.
We could avoid the duplicate method calls by memoizing,↴
Memoization ensures that a method doesn't run for the same inputs more than once by keeping a record of the results for the given inputs (usually in a hash map).
For example, a simple recursive method for computing the nth Fibonacci number:
publicstaticintfib(int n){if(n <0){thrownewIllegalArgumentException("Index was negative. No such thing as a negative index in a series.");}// base casesif(n ==0|| n ==1){return n;}
System.out.printf("computing fib(%d)\n", n);returnfib(n -1)+fib(n -2);}
We can imagine the recursive calls of this method as a tree, where the two children of a node are the two recursive calls it makes. We can see that the tree quickly branches out of control:
To avoid the duplicate work caused by the branching, we can wrap the method in a class that stores an instance variable, memo, that maps inputs to outputs. Then we simply
check memo to see if we can avoid computing the answer for any given input, and
save the results of any calculations to memo.
import java.util.Map;import java.util.HashMap;classFibber{private Map<Integer, Integer> memo =newHashMap<>();publicintfib(int n){if(n <0){thrownewIllegalArgumentException("Index was negative. No such thing as a negative index in a series.");// base cases}elseif(n ==0|| n ==1){return n;}// see if we've already calculated thisif(memo.containsKey(n)){
System.out.printf("grabbing memo[%d]\n", n);return memo.get(n);}
System.out.printf("computing fib(%d)\n", n);int result =fib(n -1)+fib(n -2);// memoize
memo.put(n, result);return result;}}
We save a bunch of calls by checking the memo:
// output of new Fibber().fib(5)
computing fib(5)
computing fib(4)
computing fib(3)
computing fib(2)
grabbing memo[2]
grabbing memo[3]
5
Now in our recurrence tree, no node appears more than twice:
Memoization is a common strategy for dynamic programming problems, which are problems where the solution is composed of solutions to the same problem with smaller inputs (as with the Fibonacci problem, above). The other common strategy for dynamic programming problems is going bottom-up, which is usually cleaner and often more efficient.
but there's a cleaner bottom-up↴
Going bottom-up is a way to avoid recursion, saving the memory cost that recursion incurs when it builds up the call stack.
Put simply, a bottom-up algorithm "starts from the beginning," while a recursive algorithm often "starts from the end and works backwards."
For example, if we wanted to multiply all the numbers in the range 1..n, we could use this cute, top-down, recursive one-liner:
publicstaticintproduct1ToN(int n){// we assume n >= 1return(n >1)?(n *product1ToN(n-1)):1;}
This approach has a problem: it builds up a call stack of size O(n), which makes our total memory cost O(n). This makes it vulnerable to a stack overflow error, where the call stack gets too big and runs out of space.
To avoid this, we can instead go bottom-up:
publicstaticintproduct1ToN(int n){// we assume n >= 1int result =1;for(int num =1; num <= n; num++){
result *= num;}return result;}
This approach uses O(1) space (O(n) time).
Some compilers and interpreters will do what's called tail call optimization (TCO), where it can optimize some recursive methods to avoid building up a tall call stack. Python and Java decidedly do not use TCO. Some Ruby implementations do, but most don't. Some C implementations do, and the JavaScript spec recently allowed TCO. Scheme is one of the few languages that guarantee TCO in all implementations. In general, best not to assume your compiler/interpreter will do this work for you.
Going bottom-up is a common strategy for dynamic programming problems, which are problems where the solution is composed of solutions to the same problem with smaller inputs (as with multiplying the numbers 1..n, above). The other common strategy for dynamic programming problems is memoization.
approach.
Breakdown
Start your free trial!
Log in or sign up with one click to get immediate access to 3 free mock interview questions
Actually, we don't support password-based login. Never have. Just the OAuth methods above. Why?
It's easy and quick. No "reset password" flow. No password to forget.
It lets us avoid storing passwords that hackers could access and use to try to log into our users' email or bank accounts.
It makes it harder for one person to share a paid Interview Cake account with multiple people.
“Once I started using Interview Cake, it became my primary resource for algorithm problems. The breakdown explanations are just excellent—so thorough and clear.
—
An