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


Here's the problem: Given a list of N coin denominations (V1, V2, ..., VN), and a sum S. Find the minimum number of coins the sum of which is 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.

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 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 i, j, min, sv;

  /* 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;
}

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 | 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
>
However, like most simple recursive implementations, it is too slow. It bogs down when S reaches the high 40's. 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 coins2.cpp):

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

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

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

  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> echo 1 5 6 10 | coins2 505
51
UNIX> echo 1 5 6 10 | coins2 5057
507
UNIX> 
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 coins3.cpp. Note how similar it is to 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)
{
  int i, j, val, min, sv;
  vector <int> cache;

  cache.resize(s+1);

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

    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];
}

Finally, there is a way to do step four if you think about it. This about that last call above -- "echo 1 5 6 10 | coins2 5057". Does your cache really need 5057 elements? If you don't see the answer, ask me in class -- this is a good test question.