Project Euler #7
2023-05-15
The problem description of Project Euler #7 is very simple and the minimum amount of work needed to just get the solution is not that much. The problem asks:
What is the 10,001st prime?
Initial Solution
My initial solution, although a bit naive, gets the job done relatively quickly for the small case:
(defn primes [n]
(loop [m 2 primes []]
(if (>= (count primes) n)
primes
(if (every? #(not= 0 (mod m %)) primes)
(recur (inc m) (conj primes m))
(recur (inc m) primes)))))
(defn euler-7 [n]
(last (primes n)))
(time (euler-7 10001)) ;"Elapsed time: 1275.52175 msecs"
;=> 104743
(time (euler-7 100001)) ;"Elapsed time: 78250.286791 msecs"
;=> 1299721
The idea of the primes
function being to calculate the first n primes by using the primes that have already
been calculated.
While this solution works, there is a lot to improve. Especially regarding time complexity.
Structure
One thing to do to enhance the structure of this code would be to remove the need for parameters in primes
by returning
an infinite lazy-seq
of primes. A direct conversion looks something like this:
(defn lazy-primes
([] (lazy-primes 2 []))
([m primes] (if (every? #(not= 0 (mod m %)) primes)
(cons m (lazy-seq (lazy-primes (inc m) (conj primes m))))
(lazy-primes (inc m) primes))))
This way our euler-7
function looks like:
(defn euler-7 [n]
(nth (lazy-primes) (dec n)))
(time (euler-7 10001)) ;"Elapsed time: 904.616083 msecs"
;=> 104743
(time (euler-7 100001)) ;"Elapsed time: 101634.307375 msecs"
;=> 1299721
A lot cleaner. A pinch faster for the small case. A lot slower for the big case.
Speed
An easy way to greatly increase the speed of the lazy-primes
function is to replace the
(every? #(not= 0 (mod m %)) primes)
with a better way to check if a number is prime. One such way is this
prime?
function:
(defn prime? [n]
(cond
(<= n 1) false
(= n 2) true
:else (every? #(not= 0 (mod n %)) (range 2 (inc (Math/sqrt n))))))
So lazy-primes
looks like this:
(defn lazy-primes
([] (lazy-primes 2 []))
([m primes] (if (prime? m)
(cons m (lazy-seq (lazy-primes (inc m) (conj primes m))))
(lazy-primes (inc m) primes))))
And performance looks like this:
(time (euler-7 10001)) ;"Elapsed time: 125.203 msecs"
;=> 104743
(time (euler-7 100001)) ;"Elapsed time: 3076.132458 msecs"
;=> 1299721
Much better. Especially for the big case.
Adding some more optimizations to the prime?
function like so:
(defn prime? [n]
(cond
(<= n 1) false
(<= n 3) true
(zero? (mod n 2)) false
(zero? (mod n 3)) false
:else (every? #(and (not= 0 (mod n %))
(not= 0 (mod n (+ 2 %))))
(range 5 (inc (Math/sqrt n)) 6))))
Yields a respectable runtime:
(time (euler-7 10001)) ;"Elapsed time: 52.351334 msecs"
;=> 104743
(time (euler-7 100001)) ;"Elapsed time: 787.097375 msecs"
;=> 1299721