Project Euler #53
8th April, 2012This was quite an interesting puzzle. It can be solved very quickly using an “idiot level solution” but at the same time it’s open to almost infinite optimisation. I chose to take the idiot level solution, since it still ran in 8 thousands of a second of CPU time, which is fast even for C. Very simple idea, we write a bang function to calculate factorials, we write an ncr function (that now I think of it should probably just have been a macro) to calculate the NCR value, which is just 3 factorials and a division. For factorials only going up to 100 we can afford to just calculate it every time. Then a pair of for loops try every value of n and r, incrementing a counter if the result is over 1000000. Print out the increment at the end. Didn’t even have to recompile.
Code can be found here: https://gist.github.com/2340044
Problem description can be found here: http://projecteuler.net/problem=53
To make up for the tiny amount of time I spent actually coding on this puzzle, let’s think about some potential optimisations that could be used to speed this program up, if we were being required to work out a similar piece of information for values up to (for example) 1000000 (which would be about 100000000 times more work for the computer I think). Firstly, and perhaps least helpfully, we can afford to push up the initial n value to 23, as stated in the problem description, the reason I chose not to do that here is that I’m always wary about being right on the upper or lower bounds when I don’t need to be. I only have to be out by one or get an equality test slightly wrong and I’m left with an almost untraceable bug. Secondly, the r value could start a little higher in our tests. Without doing any real maths it’s obvious that we could start at at least 2, because when r=1, nCr=n. Similarly the r loop could stop a little sooner as well, because the last few values are just as small as the first few.
Moving onto actual optimisation rather than just bounds tightening, I have 3 main ways that the code could be sped up. Firstly, nCr values are symmetrical, ie for any given n, nCr and nC(n-r) have the same value. This cuts in half the number of actual calculations we have to do whilst still retaining 100% accuracy. Secondly, the most system intensive part of these calculations is the factorial function, which is very difficult to optimise in itself, there isn’t really any fast way to calculate factorials. However, over the course of this program, we calculate the factorials of the same number again and again. We can quite easily store the result every time we do a calculation, then on future runs we just pull up the cached version rather than recalculating it. I’m not sure, but given how fast this code ran, I think GCC might actually have spotted this optimisation for me and implemented it, which is both clever and helpful of it.
Finally, probably the greatest possible optimisation we could have, is to remember that the nCr values are just the numbers from the appropriate rows and columns of Pascals Triangle. Though the given formula is the fastest possible way of calculating an nCr value for arbitrarily large values of n and r, when you’re calculating it for a lot of consecutive values at the same time, you can just draw the triangle in memory and use that. Drawing the triangle like this reduces the calculation of each value to a single addition. This is made even faster by the fact that once we get a value over 1000000, we know all the values that come below it in the triangle are also over 1000000, so we can just add all of those to our count, and move on.
I’m sure there’s also a lot of possible mirco-optimisation but that’s usually not worth bothering with, GCC is much better at that sort of thing than programmers are, and it usually results in messy hard to maintain code.
On a related note, this was the 50th problem I solved on Project Euler, making me officially Level 2! A great achievement I think!


