Euler Problem 37

The number 3797 has an interesting property. Being prime itself, it is possible to continuously remove digits from left to right, and remain prime at each stage: 3797, 797, 97, and 7. Similarly we can work from right to left: 3797, 379, 37, and 3.

Find the sum of the only eleven primes that are both truncatable from left to right and right to left.

NOTE: 2, 3, 5, and 7 are not considered to be truncatable primes.

Okay, I would say we start with one digit and work ourselves up. I will start with implementing a function that takes all current solutions (with one digit for example) and then tests all possible new solutions (aka all two digit primes) if the can be truncated to the one digit solutions.

This was complicated. Let's formulate it clearer. We write a function that takes a list of numbers and tests whether they are left/right trunctable to another list of numbers.

In [1]:
def get_truncatable_numbers(numbers, targets):
    s = set(targets)
    r = []
    for n in numbers:
        left_trunc = int(str(n)[1:])
        right_trunc = int(str(n)[:-1])
        if left_trunc in s and right_trunc in s:
            r.append(n)
    return r

assert(get_truncatable_numbers([37, 77, 83], [2, 3, 5, 7]) == [37, 77])

The next function returns a list of all primes with a certain number of digits n. We gonna reuse the sieve from problem 21.

In [2]:
def sieve_of_eratosthenes(limit):
    primes = []
    prospects = [n for n in range(2, limit)]
    while prospects:
        p = prospects[0]
        prospects = [x for x in prospects if x % p != 0]
        primes.append(p)
        if p * p > limit:
            break
    primes += prospects
    return primes


def get_primes_with_n_digits(n):
    lower_limit = 10**(n-1) - 1
    upper_limit = 10**n
    primes = sieve_of_eratosthenes(upper_limit)
    primes = [p for p in primes if p > lower_limit]
    return primes
    
assert(get_primes_with_n_digits(1) == [2, 3, 5, 7])
assert(len(get_primes_with_n_digits(2)) == 21)

Now we simply start with all one digit primes and work our way up using the two functions we have created. On the way up we add the numbers to our result list. For example, 97 is already a valid solution even though it is also the subset of a solution with more digits.

In [3]:
digits = 1
solutions = get_primes_with_n_digits(digits)
results = []

while solutions:
    digits += 1
    solutions = get_truncatable_numbers(get_primes_with_n_digits(digits), solutions)
    for s in solutions:
        results.append(s)
    
print(results)
[23, 37, 53, 73, 373]

This are not the eleven numbers we are looking for. The mistake we made is to not differentiate between left truncatable numbers and right truncatable numbers. For example, 397 is truncatable from the left, but not from the right and is still part of a solution. What we want to do is to work up from both directions and then compute the subset. Hence we write to new functions to check for all truncatable numbers from either left or right and then apply the algorithm from above to both of them.

In [4]:
def get_truncatable_numbers_right(numbers, targets):
    s = set(targets)
    return [n for n in numbers if int(str(n)[:-1]) in s]

def get_truncatable_numbers_left(numbers, targets):
    s = set(targets)
    return [n for n in numbers if int(str(n)[1:]) in s]

Let's redo the alogirthm.

In [5]:
digits = 1
solutions = get_primes_with_n_digits(digits)
results_left = []

while solutions:
    break
    digits += 1
    solutions = get_truncatable_numbers_left(get_primes_with_n_digits(digits), solutions)
    for s in solutions:
        results.append(s)
    
print(results_left)

digits = 1
solutions = get_primes_with_n_digits(digits)
results_right = []

while solutions:
    break
    digits += 1
    solutions = get_truncatable_numbers_right(get_primes_with_n_digits(digits), solutions)
    for s in solutions:
        results.append(s)
    
print(results_right)
[]
[]

I added the terminate symbols because this algorithm did not even terminate. This means there are fairly big primes in either direction. We change the approach from bottom up into the other direction. Let's get all primes till a certain value and just check for them. Aka brute force. For this purpose we write a function that takes a number and checks if it is truncatable in both directions.

In [6]:
primes_till_10000 = set(sieve_of_eratosthenes(10000))

def is_right_truncatable(number, targets):
    try:
        while True:
            number = int(str(number)[:-1])
            if not number in targets:
                return False
    except ValueError:
        return True

assert(is_right_truncatable(3797, primes_till_10000))
assert(is_right_truncatable(3787, primes_till_10000) == False)

def is_left_truncatable(number, targets):
    try:
        while True:
            number = int(str(number)[1:])
            if not number in targets:
                return False
    except ValueError:
        return True

assert(is_left_truncatable(3797, primes_till_10000))
assert(is_left_truncatable(3787, primes_till_10000) == False)

def is_truncatable(number, targets):
    if is_left_truncatable(number, targets) and is_right_truncatable(number, targets):
        return True
    return False

assert(is_truncatable(3797, primes_till_10000))

Let's just go for a brute force till 10k and see what we get.

In [7]:
s = [p for p in primes_till_10000 if is_truncatable(p, primes_till_10000)]
print(s)
[2, 3, 5, 7, 23, 37, 53, 73, 313, 317, 373, 797, 3137, 3797]

The one digit numbers are not relevant but this means we got only ten solutions. So we actually have to try all numbers till $10^{6}$. As it turned out we actually have to add one more digit. This is really ugly brute force. I do not like it. But seems like we got a solution.

In [8]:
primes_till_100000 = set(sieve_of_eratosthenes(1000000))
s = [p for p in primes_till_100000 if is_truncatable(p, primes_till_100000)]
print(s)
print(sum(s[4:]))
s = sum(s[4:])
assert(s == 748317)
[2, 3, 5, 7, 23, 37, 53, 73, 313, 317, 373, 797, 3137, 3797, 739397]
748317