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.