Problem 58 — Spiral Primes

solved Thu, 12 Dec 2019, 14:13 · level 2 · difficulty 6% projecteuler.net github

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 \approx 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\%$?


Problem Analysis

The spiral picture is a deliberate distraction. What actually matters is a single arithmetic observation: each time I wrap one new layer around the centre, I add exactly four corner cells - one at each corner of the larger square - and these corners lie precisely on the two diagonals. If I index layers as k = 1, 2, 3, ..., the side length at layer k is 2k+1, an odd number, and the bottom-right corner is always the odd square (2k+1)^2. This is because the spiral fills clockwise to the bottom-right and the largest value on a side-(2k+1) spiral is (2k+1)^2. The remaining three corners are reached by repeatedly subtracting 2k (the side length minus one), since each edge of the square spans exactly that many cells between consecutive corners. So the entire diagonal sequence is a closed-form function of k - I never need to build or store the grid.

The real work is counting how many diagonal entries are prime and watching when the running ratio of primes to total diagonal entries first drops below 10%. I test primality with trial division: for a candidate n, I check divisibility by odd numbers up to sqrt(n), costing O(sqrt(n)) per test. The largest corner at the answer (side length 26241) is about 26241^2 ~ 6.9 * 10^8, so each test does at most ~13000 divisor checks. I process roughly n/2 layers, and a corner near layer k costs O(k) work, so the total is O(n^2) in the side length n - comfortably within a single linear pass, which is why this sits at Level 2. The bottom-right corner is an odd square (and thus never prime for k >= 1), so I could skip it, but testing it uniformly is harmless.

Solution Approach

Layer-by-Layer Diagonal Scan (Python and C)

Both files implement the same algorithm; only the language idioms differ. The core loop walks layers from k = 1 upward. For each layer I compute side_length = 2k+1 and the step side_length - 1 = 2k. From the bottom-right corner bottom_right = side_length^2 I derive the other three by successive subtraction:

  • bottom_left = bottom_right - step
  • top_left = bottom_left - step
  • top_right = top_left - step

I keep two running accumulators: num_prime_diagonals (starting at 0) and num_diagonal_elements (starting at 1, to count the centre cell 1, which lies on both diagonals but is not prime). Each layer adds 4 to the total and adds 1 per prime corner. After updating, I check whether the ratio has dropped below the threshold and, if so, return the current side length.

In Python I expressed layer enumeration as a generator that yields a 5-tuple (side_length, bottom_right, bottom_left, top_left, top_right), and the solve function consumes it with a for loop. This separates "how to produce corners" from "when to stop". The C version is a direct port: an unbounded for (int layer = 1; ; layer++) with the same arithmetic. The crucial difference is type width. The corner values exceed the 32-bit signed range (~2.1 * 10^9) well before the answer, so the C code uses long long throughout the corner arithmetic to avoid silent overflow - note in particular (long long)side_length * side_length, which casts before multiplying so the squaring itself does not overflow. Both programs accept the threshold as a command-line argument so they can be re-run at other ratios.

workbench

Test cases

categoryinputanswer
main threshold = 0.1 26241

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0058_s0 main 26241 correct 0.127552 (28) 2.342563 (28)

Files

Tags & blogs

Blogs