Riddle 7: 10001st Prime

Riddle 7: 10001st Prime

Mia Temma's photo
Mia Temma
·Feb 8, 2022·

8 min read

The Task

By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13.

What is the 10.001st prime number?

This is a surprisingly tricky one, because the code should execute within one minute according to the Project Euler rules. I will present two easy approaches that are too slow, and a slightly more complicated one that works.


How to determine Prime Numbers

Prime numbers are only dividable by themselves and by 1. As naive approach we could test all numbers between 2 and N-1 if they are evenly dividable.

(de isPrime (N)
   (let Prime T
      (for I (- N 2)
         (when (=0 (% N (inc I)))
         (when (=0 (% N I))
            (setq Prime NIL) ) )
   Prime ) )

However, this code quickly gets very unefficient, as we need N-2 divisons per number. In order to calculate the 10.001st prime number, how many would that be?

According to online sources, the 10.000 prime is at around ~106.000. In other words, to test them all, we need more than 106.000! (faculty) calculation steps, which means 10^486650 steps (a 26497 digit number!).

wolfram.png

Since all Euler tasks are supposed to run within less than 60 seconds on a "normal" laptop, this is certainly not the way to go. Let's look for improvements.


The Sieve of Eratosthenes

Note: The following code is heavily inspired by the Rosetta Code submission for the Sieve of Eratosthenes.

The Sieve of Erosthenes is an algorithm to determine all prime numbers up to a given range. On Wikipedia, you can find a nice animation which illustrates how it works:

Sieve_of_Eratosthenes_animation.gif

First we write down all numbers, then we "remove" all numbers that are dividable by 2 (except 2 itself). Then we do the same with 3. 4 is already removed, so the next one is 5... And in the end, all numbers left are prime.


So how does that look like in PicoLisp? First, we create a list of all numbers with range. Then we set all numbers to NIL that are not prime, and as last step, we filter the list for all non-NIL values.

Let's go: First we define the list and set the first number, 1, to NIL because 1 is not prime.

(de sieve (N)
   (let Sieve (range 1 N)
      (set Sieve)

set Sieve is equal to set Sieve NIL, which sets the val of the list to zero. (If this is unexpected, read here about the set function).


Then we check the remaining list (cdr Sieve). For each number n in the list, we "remove" the n'th element. An elegant way to implement this is using the "third form of the for loop", a construct that we have already seen for example in the Rosetta Code 100 Doors task:

  • define a "temporary sieve" S and loop over it: (for (S ... S ...))
  • in each loop, the temporary sieve S is a list starting from the I^2 position: (nth Sieve (* I I)), for example (4, 5, 6, 7...) for I=2.
  • Next, we shorten the list by "jumping"I elements until the list is empty ((4, 6, ...), (6, 8, ...)): (nth (cdr S) I).
  • Each time we do that, we set the first list element to NIL with set.
(for I (cdr Sieve)
   (when I
      (for (S (nth Sieve (* I I)) S (nth (cdr S) I))
         (set S) ) ) )

As last step, all we need to do is to filter all non-NIL values:

(filter bool Sieve)

This is the finished program:

(de sieve (N)
   (let Sieve (range 1 N)
      (set Sieve)
      (for I (cdr Sieve)
         (when I
            (for (S (nth Sieve (* I I)) S (nth (cdr S) I))
               (set S) ) ) )
      (filter bool Sieve) ) )

It outputs all prime numbers up to a given N:

: (sieve 100)
-> (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)

Efficiency considerations

Our new function performs much better than the previous one, but still not good enough - N=10000 already takes up to a minute. Why is that? Instead of truly "removing" items, we just set them to NIL, but they are still included in each list operation.

In order to improve the speed, we should try to remove the non-prime elements instead of flagging them as NIL. Here we go:


Improvements: Rolling the "Wheel"

Now we use a trick that we have already seen this in the Euler Task 3.

Note: The following code is based on the Rosetta Code.


The idea is that we can get a list without any multiples of 2, 3, 5 and 7, if we continuously add up this circular list:

(setq WHEEL-2357
    (2  4  2  4  6  2  6  4
     2  4  6  6  2  6  4  2
     6  4  6  8  4  2  4  2
     4  8  6  4  6  2  4  6
     2  6  6  4  2  4  6  2
     6  4  2  4  2 10  2 10 .))

It works like this: We start with N=11 (because this is the first prime number not covered).

N = N+2 = 13
N = 13+4 = 17
N = 17+2 = 19
N = 19+4 = 23
N = 23+6 = 29
...

We receive a list that effectively removes all multiples of 2, 3, 5 and 7. Defined as function:

(de roll2357wheel (Limit)
    (let W WHEEL-2357
        (make
            (for (N 11  (<= N Limit)  (+ N (pop 'W)))
                (link N)))))

Test it:

: (roll2357wheel 100)
-> (11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)

Removing all multiples

The main issue with our previous approach was that we never removed multiple items - we just flagged them to NIL. Therefore we had many operations on the list that were actually not needed. Let's see how we can improve this.

First we can now define our wrapping function sieve. In the first step, we create a list of numbers excluding multiples of 2, 3, 5 and 7.

(de sieve (Limit)
    (let Sieve (roll2357wheel Limit)

Then we call a function remove-multiples (still to be defined), that removes the multiples of each list element destructively. We could call it for each list element, but actually we only need to go up to sqrt Limit (because the largest possible divisor is the root of Limit).

(for (P Sieve  (<= (car P) (sqrt Limit))  (cdr P))
   (remove-multiples P))

Now the tricky part: We traverse through our list and remove the multiples for each element. I. e., first we remove all multiples of 11 (starting from 121, then 132, 143...), then multiples of 13, 17 and 19 so on. Each time, we want to operate on the Sieve list directly to keep it efficient.

This is how we define remove-multiples:

  1. We take the first element of L and move up to the list until we find the first multiple M, which is N*N (there are no multiples below that because those should already be removed).

In order to move through the list, we use two pointers: the "original" pointer L and a second pointer Q which is set to (cdr L). These pointers are incremented by one in each step until (cdr L) is NIL:

(de remove-multiples (L)
   (let (N (car L)  M (* N N))
      (for (Q (cdr L) Q (cdr (setq L Q)))

Example: if our list is L=(11, 13, 17, 19...), i. e. N=11, M=121. First iteration: L points to 11 and Q points to 13, 2nd iteration: L=13, Q=17, 3rd iteration: L=17, Q=19....

Now we move up until we "hit" our first multiple Q=M. If this is the case, we "cut" M out of our list. We can do this with con, which connects the first element of L (which is one element prior to Q) to a second list. In order to skip M, we connect L to cdr Q:

(when (= (car Q) M)
   (con L (cdr Q)) )

con is a destructive operation, i. e. the pointer from the CAR of L to M is lost forever.

Note that cdr is not destructive. Therefore all elements prior to that cutting step are not damaged, because we keep a pointer to the original list in our main function.

Now that we removed one multiple, we need to increase M by N:

(when (> (car Q) M)
   (inc 'M N) )

Since some multiples might already be removed (for example 11x12 is already removed as multiple of "2"), we actually need to check this before the (= (car Q) M) condition. So this is the final code of remove-multiples:

(de remove-multiples (L)
   (let (N (car L)  M (* N N))
      (for (Q (cdr L) Q (cdr (setq L Q)))
         (when (> (car Q) M)
            (inc 'M N) )
         (when (= (car Q) M)
            (con L (cdr Q)) ) ) ) )

Note that this function has no return value as it modifies L directly.


Now we're almost done. All we need to do is to add 2, 3, 5, 7 to our prime numbers list, because our Sieve list started from 11. This is the final function:

(de sieve (Limit)
    (let Sieve (roll2357wheel Limit)
        (for (P Sieve  (<= (car P) (sqrt Limit))  (cdr P))
            (remove-multiples P))
        (append (2 3 5 7) Sieve)))

Get the 10001st prime number

The density of prime numbers is roughly logarithmic. So the easiest approach to get the 10.001st number is to aim for a number around 100.000 and manually increase if needed, until we get a 10.001st list element:

: (nth (sieve 100000) 10001 1)
-> NIL
: (nth (sieve 110000) 10001 1)
-> 104743

The final code can be downloaded here:


Sources

 
Share this