ProJeX Haskell

Posted on Thu, 5 Sep 2013

Tags: coding, mathematics, haskell, project-euler

Warning: Javascript is not supported. The mathematical formulae on this page will not display correctly.

Most of the programming languages I have learned and used so far have been from the imperative programming paradigm, which means that they describe the problem to be solved in terms what needs to be done, step by step, in order to solve it. (Object oriented programming, while often considered a separate paradigm, is essentially just a highly modular type of imperative programming.)

The alternative to the imperative paradigm is declerative programming, which describes the problem in terms of what it should accomplish, rather than how it should get there. The declerative paradigm includes logic programming languages (such as Prolog), and functional programming languages (such as Lisp or Haskell).

Functional languages are the one major paradigm which I haven't had any exposure to. Until now. I've decided that, in order to be a well-rounded coder, and also because it might be fun, I should learn a functional language; and I've chosen Haskell.

After reading the first few chapters of the strangely titled "Learn You a Haskell for Great Good!" I began thinking about what problems I can try to solve using my new language, in order to help cement my understanding. That's when I remembered about Project Euler - a series of mathematical problems intended to be solved with computer programs. I've been meaning to do Project Euler for a while now, but kept putting it off; but since Haskell is a language well suited to solving mathematical problems, I thought this was a perfect opportunity to combine the two, in ProJeX Haskell: learning Haskell through using it to solve Project Euler problems (and presenting the mathematics using LaTeX - another thing I've been meaning to learn).

Ok, so lets delve straight in…

Some general functions

Here are some general functions, that will be made use of in the following sections.

-- factorial

factorial 0 = 1

factorial n = n * factorial (n-1)

-- fibonacci sequence

fibonacci = 0 : 1 : zipWith (+) fib (tail fib)

-- combinations

choose n 0 = 1

choose 0 k = 0

choose n k = choose (n-1) (k-1) * n `div` k

-- permutations

permute n 0 = 1

permute 0 k = 0

permute n k = permute (n-1) (k-1) * n

-- sum of the digits of a number

digitSum 0 = 0

digitSum x = let (d, m) = x `divMod` 10in m + digitSum d

I originally wrote the fibonacci function in a step-wise manner (similar to the factorial function), but the version given here is faster (and nicer to look at).

A few trivial problems

It quickly becomes clear how well-suited Haskell is to solving mathematical problems. The solutions to many of these early problems can be written in a line or two!

Many of these solutions are self-explanatory. I'll just make a few notes:

Problem 9: I added the shortcut of finding a Pythagorean triplet for which (a+b+c) is a divisor of 1000, then scaled it up.

Problem 22: I use some functions from Data.Char to convert a character to its "alphabetical value". To find the solution we sort the list of names, sum the alphabetical values of the characters of each word, we then multiply each sum by its position, and finally sum the results.

Problem 31: The logic of the solution is as follows: For each coin not bigger than the total, find all ways of making the remainder with coins not bigger than the selected coin.

Problem 11

What is the greatest product of four adjacent numbers in the same direction in a 20×20 grid?

First, I represent the grid as a list of lists. I then call a recursive function on the grid. The function takes a cell as its input, and calculates a "product of four" for each direction from that cell: up-right, right, down-right, down. The recursive call does the same thing from each starting cell. The function then finds the maximum product.

First, we need to write a function to generate a Collatz sequence from a starting number. This is easy enough to do using the provided recursive definition.

I've taken a brute force approach: take all numbers under one million, generate the corresponding sequences, and keep the longest one.

I have made a small optimisation: I've started at 5000, since for any number in the lower half, there is an even number in the upper half which leads to it.

-- 14: Longest Collatz sequence (starting below 1 million)

euler14 = foldl1 (\ x y -> if snd x > snd y then x else y)

[(x, length . collatz $ x) | x <- [500000..999999]]

collatz 1 = [1]

collatz n

| even n = n : collatz (n `div` 2)

| otherwise = n : collatz (3*n + 1)

This approach takes around a minute to find the answer. There may be a quicker way to do this, perhaps one that doesn't involve generating every sequence, but this method is sufficient.

Problem 17

If all the numbers from 1 to 1000 were written out in words, how many letters would be used?

For this problem, I took a slightly more general approach than necessary. I wrote a numberWords function to write out in words any number under one million, including spaces and punctuation. I then used this function to write all numbers in the required range, removed the undesirable characters, and counted the length of the final string.

The numberWords function works recursively, getting the thousands part of the number (if appropriate), then the hundreds, tens and units, respectively.

-- 17: Number letter counts (number of letters used to write 1 - 1000 in words)

Problem 19

How many Sundays fell on the first of the month during the twentieth century?

Once again, a more general solution than strictly necessary. The function dayOfTheWeek will give the day for any date in history (or, any date since Gregorian calendar was introduced). This function is then used to check by brute force which "first of the month"s fell on a Sunday.

-- 19: Counting Sundays (Number of Sundays on the first of the month during the twentieth century)

Now, it should be clear that the diagonals added for the nth spiral are an arithmetic progression continuing on from the previous number in the odd/even sequence, with a common difference of \(n-1\).That is, if \(D_{n-2} = [i, j, k, l]\), then \(D_{n} = [l+(n-1), l+2(n-1), l+3(n-1), l+4(n-1)]\).For example, \(D_{7} = [25+(7-1), 25+2(7-1), 25+3(7-1), 25+4(7-1)] = [31, 37, 43, 49]\).

That's all we need to know to generate all the diagonals for our 1001 by 1001 spiral, and then sum them to get our answer.

-- 28: Number spiral diagonals (sum of the diagonals in a 1001 by 1001 ulam spiral)

euler28 = sum . spiralDiags $ 1001

spiralDiags 0 = [0]

spiralDiags 1 = [1]

spiralDiags n = map ((+ head prev) . (*(n-1))) [4,3,2,1] ++ prev

where prev = spiralDiags (n-2)

That's all for now. There will be more ProJeX Haskell posts to come in the future.