You can compile the programs with "make coins".
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:
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>
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>
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.
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.