I had some free time these past few days, so I got back to Project Euler: I like fooling around with the problems there, particularly because the process reminds me of what it felt like when I begun working with computers; the ecstasy of "pure" problem solving, without the nasty side-effects of dealing with clients :‑)
Hey, on occasion I am even known to pretend to be a scientist :‑)
The problem I've enjoyed most so far, is problem 31:
In England the currency is made up of pound, £, and pence, p, and there are eight coins in general circulation: 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p).Hmm...It is therefore possible to make £2 in the following way:
1x£1, 1x50p, 2x20p, 1x5p, 1x2p, 3x1p
How many different ways can £2 be made using any number of coins?
Being the trigger-happy engineer that I am, I quickly tried to brute-force:
#include <stdio.h> #include <signal.h> // We want to accumulate 200 cents (2 pounds) #define TARGET 200 int coins[] = {1, 2, 5, 10, 20, 50, 100, 200}; long long total = 0LL; int newCoin(int s) { if (s == TARGET) { total ++; // landed right on 200 cents, increase total return 1; // and report "break out of the loop" } else if (s>TARGET) { // we overflowed, go back and report "break out of the loop" return 1; } else { // recurse, adding each available coin in turn for(int idx=0; idx<sizeof(coins)/sizeof(coins[0]); idx++) { int earlyAbort = newCoin(s+coins[idx]); // if the callee reported non-zero, we break if (earlyAbort) return 0; } } return 0; } void pr(int i) { // SIGINT (Ctrl-C) triggered reports printf("\n%lld\n", total); } int main() { signal(SIGINT, pr); newCoin(0); // start the game printf("%lld\n", total); }
Sure, sure, I am not handling the signals properly, printf is not re-entrant - but who cares, I just want to quickly see if this solves the problem or not; Ctrl-C will print how high total is, so trying it out...
ttsiod@elrond:tmp$ g++ -O2 problem31.brute.cc ttsiod@elrond:tmp$ ./a.out ^C 32378168 ^C 51764088 ^C 81886592 ^C 168584448 ^C 225451092 ^Z [1]+ Stopped ./a.out ttsiod@elrond:tmp$ kill %1 [1]+ Killed ./a.outOoops. This thing grows rapidly (too rapidly)!
And it doesn't seem to finish. After 1 minute of running...
To make matters worse, the speed at which total goes up makes it clear that I am being an idiot: the blind recursion will obviously measure 1p+2p as a different combination to 2p+1p. Order is not supposed to count; the problem statement says "how many different ways", and this is clearly violating it.
Target in cents | Using only 1p | Using <= 2p | Using <= 5p | ... |
1 | 1 | ... |
Only one way - so, fill up the cell of column "Using only 1p" with 1.
Moving on - what if I could use coins less than or equal to 2p?
Target in cents | Using only 1p | Using <= 2p | Using <= 5p | ... |
1 | 1 | 1 | ... |
So all the first line fills up with 1s:
Target in cents | only 1p | <= 2p | <= 5p | <= 10p | <= 20p | <= 50p | <= 100p | <= 200p |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Target in cents | only 1p | <= 2p | <= 5p | <= 10p | <= 20p | <= 50p | <= 100p | <= 200p |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 1 |
When using coins less than or equal to 2p, however, we can also do it via a single 2p coin - so there are now 2 ways:
Target in cents | only 1p | <= 2p | <= 5p | <= 10p | <= 20p | <= 50p | <= 100p | <= 200p |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
Target in cents | only 1p | <= 2p | <= 5p | <= 10p | <= 20p | <= 50p | <= 100p | <= 200p |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
3 | 1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
4 | 1 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
5 | 1 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
OK, we can now write it down in Python...
#!/usr/bin/env python # the 8 coins correspond to 8 columns coins = [1, 2, 5, 10, 20, 50, 100, 200] TARGET=200 matrix = {} for y in xrange(0, TARGET+1): # There is only one way to form a target sum N # via 1-cent coins: use N 1-cents! matrix[y, 0] = 1 # equivalent to matrix[(y,0)]=1 for y in xrange(0, TARGET+1): print y, ":", 1, for x in xrange(1, len(coins)): matrix[y, x] = 0 # Is the target big enough to accomodate coins[x]? if y>=coins[x]: # If yes, then the number of ways to form # the target sum are obtained via: # # (a) the number of ways to form this target # using ONLY coins less than column x # i.e. matrix[y][x-1] matrix[y, x] += matrix[y, x-1] # plus # (b) the number of ways to form this target # when USING the coin of column x # which means for a remainder of y-coins[x] # i.e. matrix[y-coins[x]][x] matrix[y, x] += matrix[y-coins[x], x] else: # if the target is not big enough to allow # usage of the coin in column x, # then just copy the number of ways from the # column to the left (i.e. with smaller coins) matrix[y, x] = matrix[y, x-1] print matrix[y, x], print
ttsiod@elrond:tmp$ python code/problem31.py 0 : 1 1 1 1 1 1 1 1 1 : 1 1 1 1 1 1 1 1 2 : 1 2 2 2 2 2 2 2 3 : 1 2 2 2 2 2 2 2 4 : 1 3 3 3 3 3 3 3 5 : 1 3 4 4 4 4 4 4 6 : 1 4 5 5 5 5 5 5 7 : 1 4 6 6 6 6 6 6 8 : 1 5 7 7 7 7 7 7 9 : 1 5 8 8 8 8 8 8 10 : 1 6 10 11 11 11 11 11 11 : 1 6 11 12 12 12 12 12 ... 195 : 1 98 1980 14140 43450 62156 65934 65934 196 : 1 99 2000 14350 44275 63500 67425 67425 197 : 1 99 2020 14560 45100 64844 68916 68916 198 : 1 100 2040 14770 45925 66188 70407 70407 199 : 1 100 2060 14980 46750 67532 71898 71898 200 : 1 101 2081 15211 47696 69118 73681 73682
The above method, in case you've never seen it before, is called dynamic programming - and just as was done here, can be used to quickly solve problems that are impossible to brute force.
OK, project Euler... one more down, three hundred to go! :‑)
Index | CV | Updated: Sat Oct 8 12:33:59 2022 |