CS302 Lecture Notes - Dynamic Programming
Example Program #2: The Coin Problem


Here's the problem: You are given a list of N coin denominations (V1, V2, ..., VN), and a sum S. Your job is to find the minimum number of coins that sum to S (we can use as many coins of one denomination as we want), or report that it's not possible to select coins in such a way that they sum up to S.

You can compile the programs with "make coins".


Step One

We start with Step 1: finding the recursion. Often the best way to do this is to try some examples. I think its easier to think of postage stamps instead of coins, because they can come in wacky denominations. And then I think of the sum as a total amount of postage, and I'm trying to put stamps on the package to equal the amount of postage. So, for example, suppose our denominations are 1, 5, 6 and 10, and our sum is 11. The minimum way of constructing the sum is one 5 and one 6, or one 10 and one 1. Since the problem asks for the minimum number of "coins", the answer is 2. Similarly, if our sum is 18, the answer is three: three 6's.

Let's make a quick table of sums and answers:

S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Answer 1 2 3 4 1 1 2 3 4 1 2 2 3 4 2 2 3 3
Coins 1 1,1 1,1,1 1,1,1,1 5 6 6,1 6,1,1 6,1,1,1 10 5,6 6,6 6,6,1 6,6,1,1 10,5 10,6 10,6,1 6,6,6

There doesn't appear to be a nice pattern, but think recursively. If I want to make the sum S, and my coins are 1, 5, 6 and 10, then I can make the sum in four ways:

So, let's suppose my procedure is M(S). If I define it recursively, then my answer is going to be the minimum of M(S-1)+1, M(S-5)+1, M(S-6)+1 and M(S-10)+1.

See how to write the procedure? You loop through all the values, making recursive calls. It's in src/coins1.cpp. I use s+1 as a sentinel value for the minimum, and if the sum cannot be constructed, then I return -1.

#include <iostream>
#include <cstdlib>
#include <vector>
using namespace std;

class Coins {
  public:
    vector <int> v;
    int M(int i);
};

int Coins::M(int s)
{
  int j, min;
  size_t i;

  /* We initialize the minimum by sentinelizing it to an impossible value. */

  min = s+1;

  /* Loop through all of the coins. */

  for (i = 0; i < v.size(); i++) {

    /* If our sum equals a coin, then we're done -- return one. */

    if (s == v[i]) return 1;

    /* Otherwise, simulate using the coin by calling M() on the
       sum minus the coin's value.   If that's better than 
       our current minimum, update the minimum. */

    if (s > v[i]) {
      j = M(s-v[i]) + 1;
      if (j != 0 && j < min) min = j;
    }
  }

  /* The min equals the sentinel, then it's impossible, return -1. 
     Otherwise, return the minimum. */

  if (min == s+1) return -1;
  return min;
}

int main(int argc, char **argv)
{
  Coins c;
  int i;
  int sum;

  if (argc != 2) {
    cerr << "usage: coins s -- values on standard input\n";
    exit(1);
  }
  sum = atoi(argv[1]);
  while (cin >> i) c.v.push_back(i);

  cout << c.M(sum) << endl;
  return 0;
}

A quick test shows that it works:

UNIX> sh
> for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ; do echo $i `echo 1 5 6 10 | bin/coins1 $i`; done
1 1
2 2
3 3
4 4
5 1
6 1
7 2
8 3
9 4
10 1
11 2
12 2
13 3
14 4
15 2
16 2
17 3
18 3
UNIX>
However, like most simple recursive implementations, it is too slow. It bogs down when S reaches the high 50's:
UNIX> time sh -c "echo 1 5 6 10 | bin/coins1 54"
7

real	0m1.107s
user	0m1.101s
sys	0m0.005s
UNIX> time sh -c "echo 1 5 6 10 | bin/coins1 58"
7

real	0m4.696s
user	0m4.684s
sys	0m0.007s
UNIX> 

Step Two

So, we do a simple memoization. This looks pretty much just like the memoization in the Fibonacci numbers, although I use two sentinel values in the cache: If cache[s] equals -2, then I have not calculated the value. If cache[s] equals -1, then it is impossible to make the sum s. Here is the M() method (in src/coins2.cpp):

int Coins::M(int s)
{
  int j, min;
  size_t i;

  /* Create the cache if this is our first call.  Return the value from the cache
     if we've done this one already. */

  if ((int) cache.size() <= s) cache.resize(s+1, -2);
  if (cache[s] != -2) return cache[s];

  /* Base case -- if s is zero, we need zero coins. */

  if (s == 0) {
    cache[s] = 0;
    return 0;
  }

  /* Otherwise, the code is nearly identical to the previous version.
     The only difference is that we put our answer into the cache. */

  min = s+1;

  for (i = 0; i < v.size(); i++) {
    if (s >= v[i]) {
      j = M(s-v[i]) + 1;
      if (j != 0 && j < min) min = j;
    }
  }
  if (min == s+1) min = -1;
  cache[s] = min;
  return min;
}

This is much faster:

UNIX> time sh -c "echo 1 5 6 10 | bin/coins2 58"
7

real	0m0.009s
user	0m0.003s
sys	0m0.004s
UNIX> time sh -c "echo 1 5 6 10 | bin/coins2 5800"
580

real	0m0.007s
user	0m0.003s
sys	0m0.004s
UNIX> 

Step 3

For Step 3, as with the Fibonacci numbers, we make the observation that we are always making recursive calls from larger s to smaller s. Thus, we can build the cache from low to high values of s without using recursion. The code is in src/coins3.cpp. Note how similar it is to src/coins2.cpp - the difference is that it looks into the cache instead of making recursive calls, and it builds the cache from low to high rather than making recursive calls from high to low. I also removed the cache from the Coins class and made it a local variable to M():

int Coins::M(int s)         // Now we simply build the cache from low to high:
{                           // No recursion necessary!
  int j, val, min;
  vector <int> cache;
  size_t i;

  cache.resize(s+1);
  cache[0] = 0;
  
  for (j = 1; j <= s; j++) {
    min = s+1;

    // This is very similar to the recursive version, only instead of
    // making a recursive call, we simply grab the answer from the cache.

    for (i = 0; i < v.size(); i++) {
      if (j >= v[i]) {
        val = cache[j-v[i]] + 1;
        if (val != 0 && val < min) min = val;
      }
    }
    if (min == s+1) min = -1;
    cache[j] = min;
  }
  return cache[s];
}

Interestingly, this is one of the rare times where Step 3 may not actually improve performance from Step 2. To help you think about why, what if the coins vector is { 1000, 5000, 6000, 10000 } and you make S equal to, say, 4,111,000?

UNIX> time sh -c "echo 1000 5000 6000 10000 | bin/coins2 4111000"
412

real	0m0.015s
user	0m0.007s
sys	0m0.007s
UNIX> time sh -c "echo 1000 5000 6000 10000 | bin/coins3 4111000"
412

real	0m0.036s
user	0m0.027s
sys	0m0.008s
UNIX> 
The reason coins3 is slower is that it calculates in every value of the cache, while coins2 only calculates multiples of 1000. This is rare, but it's a good thing for you to understand.

Step Four

Finally, there is a way to do step four if you think about it. Think about when we called:
UNIX> time sh -c "echo 1 5 6 10 | bin/coins2 5800"
Does your cache really need 5800 elements? If you don't see the answer, ask me in class -- this is a good test question. Use the "Step Four" part of the Fibonacci numbers for inspiration.