60 Days of Euler in F# - Problem 50

The Problem

The prime 41, can be written as the sum of six consecutive primes:

41 = 2 + 3 + 5 + 7 + 11 + 13

This is the longest sum of consecutive primes that adds to a prime below one-hundred.

The longest sum of consecutive primes below one-thousand that adds to a prime, contains 21 terms, and is equal to 953.

Which prime, below one-million, can be written as the sum of the most consecutive primes?

The Solution

Problem 50! I think after solving this one, I earn some sort of prize. I'm hoping for a luxury vacation, but I'm not holding my breath.

The first thing we need is the list of primes below 1000000. We'll start by stealing some code from problem 35 to generate those.

let intsqrt i = int(sqrt(float i))

let isPrime i =
    if i <= 1 then false
    elif i = 2 then true
    elif (i &&& 1) = 0 then false
    else
        let sr = intsqrt i
        seq { 3..2..sr } |> Seq.forall (fun f -> i%f<>0)

let rec nextPrime x =
    match x with
    | x when isPrime x -> x
    | x -> nextPrime (x + 1)


let getPrimesBelow max =
    let primeGenerator candidate =
        if candidate > max then
            None
        elif candidate = 2 then
            Some(2,3)
        else
            let next = nextPrime candidate
            if next >= max then
                None
            else
                Some(next,next+2)

    Seq.unfold primeGenerator 2

[<Literal>]
let target = 1000000

let primes = getPrimesBelow target |> List.ofSeq

Now we have a list of primes. The plan of attack is to go through each and find the starting prime of the sequence that, when summed, adds up to a prime number n.

let isSumOfConsecutivePrimes n =
    let rec isSum candidates sum len =
        match candidates with
        | [] -> None
        | x::xs ->
            let newSum = sum + x
            let newLen = len + 1
            if newSum = n then Some(newLen)
            elif newSum > n then None
            else
                isSum xs newSum newLen

    let rec findStartingPrime candidates =
        match candidates with
        | [] -> None
        | x::_ when x > n -> None
        | x::_ when x = n -> Some(n,1)
        | x::xs -> 
            match isSum xs x 1 with
            | Some l -> Some(n,l)
            | _ -> findStartingPrime xs

    findStartingPrime primes

isSumOfConsecutivePrimes starts by looping through each prime and determining if that prime is the start of the sequence we're looking for. The internal function isSum takes the list of subsequent primes, the current sum, and the length of the current sequence. If it gets to the end of the list of primes, it returns None. Otherwise, if it hits the sum we're looking for, it returns the length of the sequence that generated the sum.

This is a brute force way of solving the problem. I am certain there are other, better ways to do it. But this one works.

Full disclosure: in order to get this to run under the 1-minute time limit, I had to compile it with full code optimizations turned on. To get it to run in under a minute in FSI, I had to run it in parallel.

Here is the single-threaded code that returns the answer:

#time
primes
|> Seq.map isSumOfConsecutivePrimes
|> Seq.choose (fun x -> x)
|> Seq.maxBy (fun (_,len) -> len)
|> fst
#time

And here's the parallel version:

let isSumOfConsecutivePrimesAsync n = async {
    return isSumOfConsecutivePrimes n
}

#time
primes
|> Seq.map isSumOfConsecutivePrimesAsync
|> Async.Parallel
|> Async.RunSynchronously
|> Seq.choose(fun x -> x)
|> Seq.maxBy (fun (_,len) -> len)
|> fst
#time
Other Posts in This Series