I'm trying to solve the problems of Project Euler with Haskell within less than 10s with optimized code (ghc -O2
). Unfortunately, I'm struggling to find a correct algorithm for Problem 44, which proves the correctness of the solution without setting a predefined upper limit (setting a limit and omitting the proof yields a fraction of a second... unfortunately, you'll almost only find this solution while searching the web, which might be due to a slightly different wording of the question in the past).
Pentagonal numbers are generated by the formula, \$P_n=n(3n−1)/2\$. The first ten pentagonal numbers are:
\$1, 5, 12, 22, 35, 51, 70, 92, 117, 145, \ldots\$
It can be seen that \$P_4 + P_7 = 22 + 70 = 92 = P_8\$. However, their difference, \$70 − 22 = 48\$, is not pentagonal.
Find the pair of pentagonal numbers, \$P_j\$ and \$P_k\$, for which their sum and difference are pentagonal and \$D = |P_k − P_j|\$ is minimised; what is the value of \$D\$?
The way I want to go is straightforward. Check every possible difference D
, till a solution is found. Unfortunately, this means, that a total of around \$1.2\cdot10^{9}\$ checks for pentagonal numbers have to be made.
My solution is the following:
-- calculate the n-th pentagonal number
pn :: Int -> Int
pn n = n*(3*n-1) `quot` 2
-- check if number is pentagonal
ispn :: Int -> Bool
ispn x = x == pn n
where n = round $ (\x -> 1/6 * (1 + sqrt(1+24*x))) $ fromIntegral x
-- solution finder
-- l > difference index
-- m > prior limit of list with relevant pentagonal numbers
-- pl > list of pentagonal numbers
finder :: Int
finder = finder' 1 0 []
finder' :: Int -> Int -> [Int] -> Int
finder' l m pl = if length fpl /= 0
then l
else finder' (l+1) nm npl
where pnl = pn l
nm = (pnl - 1) `div` 3 + 1 -- +1 as backup
npl = (map pn [(m+1)..nm]) ++ pl
-- pnl -> difference
-- x -> smaller number
-- x+pnl -> larger number
-- 2*x+pnl -> sum
fpl = filter (\x -> ispn (x+pnl) && ispn (2*x+pnl)) npl
main = print finder
As you can see, the code goes through every pentagonal difference \$D=P_l\$ and checks all relevant numbers by calculating the upper limit for the smaller of both numbers (index nm
; above, the distance between two pentagonal numbers is larger than \$D\$) and testing the resulting larger number and sum for their pentagonality.
Profiling with
ghc -O2 -fforce-recomp -rtsopts -prof -fprof-auto main.hs
./main +RTS -sstderr -p
yields an increase of runtime by 200% (from 34s to 103s) and reports the following costs:
COST CENTRE MODULE %time %alloc finder'.fpl.\ Main 30.7 0.0 ispn Main 23.7 0.0 ispn.n Main 22.5 0.0 ispn.n.\ Main 12.6 0.0 finder'.fpl Main 10.0 0.0 finder'.npl Main 0.1 99.9
I already tried to convert the check for pentagonality to a set lookup, but this didn't really help because this will grow up to 2 million elements. Also, converting the to-be-filtered list to an array or set didn't work as expected.
For a given difference \$D\$ (given by the pentagonal number \$P_l\$ which is pnl = pn l
in the code), we know that starting with a given \$k\$ (nm
in the code) the difference between \$P_k\$ and \$P_j\$ (\$j > k\$) will always be larger than \$D\$. This starting index can be calculated as \$k = \lfloor\frac{D-1}{3}\rfloor + 1\$.
Is there any way to squeeze some additional time out of this code, preferably while sticking to base libraries?