Problem 37 — Truncatable Primes

solved Wed, 13 Nov 2019, 12:33 · level 1 · difficulty 3% projecteuler.net github

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.


Problem Analysis

A truncatable prime is a prime number that stays prime as you repeatedly strip digits from one end. Take 3797: it is prime, and so are 797, 97, and 7 (stripping from the left), as well as 379, 37, and 3 (stripping from the right). A number qualifies only if every prefix and every suffix of its decimal representation is also prime. The single-digit primes 2, 3, 5, and 7 are explicitly excluded, so every truncatable prime has at least two digits. The problem guarantees exactly eleven exist, which gives me a clean stopping condition for an open-ended search.

The observation that unlocks an efficient solution is that truncation is a structural property of the digit string, not an arithmetic relationship. If I keep a running set of every prime I have generated, stored as decimal strings, then testing a candidate reduces to slicing its string at each cut point and asking whether both pieces are already in the set. That converts an apparently expensive recursive primality check into a handful of average-O(1) set lookups. Because truncatable primes are sparse (all eleven lie below one million), a generate-and-test strategy works: stream primes in increasing order, store each one, and test each multi-digit prime until eleven qualify. Generating primes incrementally is a Sieve of Eratosthenes variant that runs in roughly O(n log log n) over the range scanned, while holding only about pi(sqrt(n)) entries in memory - where pi is the prime-counting function - rather than allocating a full boolean array up to an unknown bound.

Solution Approach

Incremental Sieve with String-Set Membership Test (Python and C)

Both files implement the same algorithm. I describe it once, then note the language-specific differences.

Step 1 - Incremental prime generation. I cannot pre-size a classical Sieve of Eratosthenes because I do not know in advance how large the eleventh truncatable prime is. Instead I use the dict-based incremental sieve attributed to David Eppstein. A map called composites associates each upcoming composite number with one of its prime factors. The invariant is: by the time I examine candidate n, every composite below n already has an entry. If n is absent from the map, it is prime; I yield it and schedule its first crossing by recording n*n -> n. If n is present, it is composite; I remove its entry, take its prime factor p, and advance to the next odd multiple of p not yet claimed by stepping in increments of 2*p. Stepping by 2*p keeps the stream on odd multiples and skips all even numbers, which after the initial yield 2 can never be prime.

Step 2 - Maintain the set of prime strings. The instant a prime is discovered, I convert it to its decimal string and insert it into a set. This is the data-representation decision that makes the whole approach fast: truncation is naturally a string operation, so I pay the integer-to-string conversion exactly once per prime and then work entirely in string space.

Step 3 - Truncation test. For a prime string s of length L, I scan cut positions i = 1 .. L-1. At each cut, s[:i] is what remains after right-truncation and s[i:] is what remains after left-truncation. Both must already be in the prime-string set. Because primes are generated in increasing numerical order, and every prefix and suffix of a number is strictly smaller than the number itself, those smaller primes are guaranteed to have been seen and stored already - this ordering guarantee is what makes the membership test sound. The first failing cut rejects the candidate immediately. Single-digit primes are skipped with an explicit guard. Once eleven candidates pass, the loop stops and I return their sum.

Language differences. The Python solution expresses the prime stream as a yield-based generator and uses Python's built-in dict and set. Its truncation test uses a not any(...) idiom over a comprehension - readable and short-circuiting, though slightly heavier than a hand-written early-exit loop. The C solution re-implements both data structures from scratch: an open-addressing hash table with linear probing for the integer composites map, and a second hash table of fixed-length character arrays for the prime-string set, hashed with the djb2 function. The C code also guards against integer overflow when computing n*n and handles hash deletion by re-inserting the displaced probe chain. These manual structures are the entire reason for the speed difference; the algorithm is byte-for-byte identical.

workbench

Test cases

categoryinputanswer
main 748317

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0037_s0 main 748317 correct 0.011908 (63) 0.172225 (63)

Files

Tags & blogs

Blogs