Spiral primes (Euler Problem 58)

Back to overview.

https://projecteuler.net/problem=58

Starting with 1 and spiralling anticlockwise in the following way, a square spiral with side length 7 is formed.

37 36 35 34 33 32 31
38 17 16 15 14 13 30
39 18  5  4  3 12 29
40 19  6  1  2 11 28
41 20  7  8  9 10 27
42 21 22 23 24 25 26
43 44 45 46 47 48 49

It is interesting to note that the odd squares lie along the bottom right diagonal, but what is more interesting is that 8 out of the 13 numbers lying along both diagonals are prime; that is, a ratio of 8/13 ≈ 62%.

If one complete new layer is wrapped around the spiral above, a square spiral with side length 9 will be formed. If this process is continued, what is the side length of the square spiral for which the ratio of primes along both diagonals first falls below 10%?

In [1]:
def get_last_corner_value(side_length):
    return side_length * side_length

assert(get_last_corner_value(1) == 1)
assert(get_last_corner_value(3) == 9)
assert(get_last_corner_value(5) == 25)


def get_corner_values(side_length):
    if side_length == 1:
        return [1]
    return [get_last_corner_value(side_length) - i * (side_length - 1)
            for i in range(0, 4)][::-1]

assert(get_corner_values(1) == [1])
assert(get_corner_values(3) == [3, 5, 7, 9])
assert(get_corner_values(5) == [13, 17, 21, 25])
assert(get_corner_values(7) == [31, 37, 43, 49])


def get_diagonal_values(side_length):
    return [corner_value
        for length in range(1, side_length + 1, 2)
        for corner_value in get_corner_values(length)
    ]

assert(get_diagonal_values(1) == [1])
assert(get_diagonal_values(3) == [1, 3, 5, 7, 9])
assert(get_diagonal_values(5) == [1, 3, 5, 7, 9, 13, 17, 21, 25])
In [2]:
def expmod(base, exp, m):
    if exp == 0:
        return 1
    if (exp % 2 == 0):
        return (expmod(base, exp // 2, m) ** 2 % m)
    return (base * expmod(base, exp - 1, m) % m)
    

def fermat_test(n):
    if n < 40:
        return n in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 39]
    a = n - 3
    if not expmod(a, n, n) == a:
        return False
    a = n - 5
    if not expmod(a, n, n) == a:
        return False
    a = n - 7
    if not expmod(a, n, n) == a:
        return False
    a = n - 11
    if not expmod(a, n, n) == a:
        return False
    a = n - 13
    if not expmod(a, n, n) == a:
        return False
    a = n - 17
    if not expmod(a, n, n) == a:
        return False
    a = n - 19
    if not expmod(a, n, n) == a:
        return False
    a = n - 23
    if not expmod(a, n, n) == a:
        return False
    a = n - 29
    if not expmod(a, n, n) == a:
        return False
    a = n - 31
    if not expmod(a, n, n) == a:
        return False
    a = n - 37
    if not expmod(a, n, n) == a:
        return False
    a = n - 39
    if not expmod(a, n, n) == a:
        return False
    return True

def trial_division(n):
    a = []  
    if n % 2 == 0:
        a.append(2)
    while n % 2 == 0:
        n //= 2
    f = 3
    while f * f <= n:
        if n % f == 0:
            a.append(f)
            while n % f == 0:
                n //= f
        else:
            f += 2   
    if n != 1:
        a.append(n)
    return a

def is_prime(n):
    if n == 1:
        return False
    return trial_division(n)[0] == n

assert(fermat_test(3))
assert(fermat_test(107))
assert(fermat_test(108) == False)
assert(fermat_test(109))
In [3]:
def get_solution():
    n = 1
    count_primes = 0
    count_total = 0
    while True:
        for v in get_corner_values(n):
            count_total += 1
            if is_prime(v):
                count_primes += 1
        ratio = count_primes / count_total
        if ratio != 0 and ratio < 0.10:
            print("n: {} count_total: {} count_primes: {} ratio: {}".format(n, count_total, count_primes, ratio))
            return n
        n += 2

s = get_solution()
n: 26241 count_total: 52481 count_primes: 5248 ratio: 0.09999809454850327
In [4]:
print(s)
assert(s == 26241)
26241

I actually got different results for the Fermat test and for the prime test which relies on reliable computation. I will actually try to solve the problem with ramdonized Fermat test now.

In [5]:
import random

def is_prime(n):
    if n == 1:
        return False
    for _ in range(100):
        a = n - random.randint(1, n - 1)
        if not expmod(a, n, n) == a:
            return False
    return True

assert(fermat_test(3))
assert(fermat_test(107))
assert(fermat_test(108) == False)
assert(fermat_test(109))
In [6]:
s = get_solution()
n: 26641 count_total: 53281 count_primes: 5328 ratio: 0.09999812315834913

Something seems to be off with my Fermat test...

Seems like there are systematic errors with the Fermat tests. Certain primes cannot be deteced.

Try this algorithm instead https://en.wikipedia.org/wiki/Miller–Rabin_primality_test.

In [ ]: