Problem 64 — Odd Period Square Roots

solved Wed, 11 Mar 2020, 15:39 · level 3 · difficulty 9% projecteuler.net github

All square roots are periodic when written as continued fractions and can be written in the form:

$$\sqrt{N}=a_0 + \dfrac 1 {a_1 + \dfrac 1 {a_2 + \dfrac 1 {a_3 + \dots}}}$$

For example, let us consider $\sqrt{23}:$

$$\sqrt{23} = 4 + \sqrt{23}-4=4 + \dfrac 1 {\dfrac 1 {\sqrt{23}-4}} = 4+\dfrac 1 {1 + \dfrac{\sqrt{23}-3}7}$$

If we continue we would get the following expansion:

$$\sqrt{23}=4 + \dfrac 1 {1 + \dfrac 1 {3+ \dfrac 1 {1 + \dfrac 1 {8+ \dots}}}}$$

The process can be summarised as follows:

$$\begin{align} \quad \quad a_0 &= 4, \frac 1 {\sqrt{23}-4}=\frac {\sqrt{23}+4} 7=1+\frac {\sqrt{23}-3} 7 \\ \quad \quad a_1 &= 1, \frac 7 {\sqrt{23}-3}=\frac {7(\sqrt{23}+3)} {14}=3+\frac {\sqrt{23}-3} 2 \\ \quad \quad a_2 &= 3, \frac 2 {\sqrt{23}-3}=\frac {2(\sqrt{23}+3)} {14}=1+\frac {\sqrt{23}-4} 7 \\ \quad \quad a_3 &= 1, \frac 7 {\sqrt{23}-4}=\frac {7(\sqrt{23}+4)} 7=8+\sqrt{23}-4 \\ \quad \quad a_4 &= 8, \frac 1 {\sqrt{23}-4}=\frac {\sqrt{23}+4} 7=1+\frac {\sqrt{23}-3} 7 \\ \quad \quad a_5 &= 1, \frac 7 {\sqrt{23}-3}=\frac {7 (\sqrt{23}+3)} {14}=3+\frac {\sqrt{23}-3} 2 \\ \quad \quad a_6 &= 3, \frac 2 {\sqrt{23}-3}=\frac {2(\sqrt{23}+3)} {14}=1+\frac {\sqrt{23}-4} 7 \\ \quad \quad a_7 &= 1, \frac 7 {\sqrt{23}-4}=\frac {7(\sqrt{23}+4)} {7}=8+\sqrt{23}-4 \end{align}$$

It can be seen that the sequence is repeating. For conciseness, we use the notation $\sqrt{23}=[4;(1,3,1,8)]$, to indicate that the block (1,3,1,8) repeats indefinitely.

The first ten continued fraction representations of (irrational) square roots are:

$\quad \quad \sqrt{2}=[1;(2)]$, period=$1$
$\quad \quad \sqrt{3}=[1;(1,2)]$, period=$2$
$\quad \quad \sqrt{5}=[2;(4)]$, period=$1$
$\quad \quad \sqrt{6}=[2;(2,4)]$, period=$2$
$\quad \quad \sqrt{7}=[2;(1,1,1,4)]$, period=$4$
$\quad \quad \sqrt{8}=[2;(1,4)]$, period=$2$
$\quad \quad \sqrt{10}=[3;(6)]$, period=$1$
$\quad \quad \sqrt{11}=[3;(3,6)]$, period=$2$
$\quad \quad \sqrt{12}=[3;(2,6)]$, period=$2$
$\quad \quad \sqrt{13}=[3;(1,1,1,1,6)]$, period=$5$

Exactly four continued fractions, for $N \le 13$, have an odd period.

How many continued fractions for $N \le 10\,000$ have an odd period?


Problem Analysis

Every quadratic irrational - including the square root of any non-square integer - has a continued fraction expansion that is eventually periodic. A continued fraction writes a number as an integer part plus the reciprocal of another such expansion, nested indefinitely; for square roots this nesting settles into a repeating block. My task is to count how many N up to 10,000 have an odd-length repeating block. Perfect squares are excluded outright: their roots are rational, the expansion terminates, and worse, the recurrence below would divide by zero on the first step.

The key observation that makes the problem tractable is that the algorithm for extracting the coefficients - the standard square-root periodic continued fraction recurrence - keeps the entire state in three small integers. At each step the current remainder has the form (sqrt(N) + m) / d, and from (m, d) I read off the next coefficient a. Crucially m is bounded by sqrt(N) and d divides N - m^2 exactly, so the triple (m, d, a) ranges over a finite set. By the pigeonhole principle, the sequence of triples must eventually repeat, and the first repeat closes the cycle: the number of distinct triples seen is the period. Since the period for N up to 10,000 is at most a few hundred, each inner loop runs in roughly O(sqrt(N)) steps, and the overall cost is about O(N_max * sqrt(N_max)) - on the order of 10^6 integer operations, trivially fast.

Solution Approach

Continued-Fraction Period Detection via State-Triple Cycle

Both the Python (p0064_s0.py) and C (p0064_s0.c) files implement the identical algorithm; I describe it once and note the language differences afterward.

Step 1 - Compute a0. For each candidate N I compute a0 = floor(sqrt(N)). If a0^2 == N then N is a perfect square and I skip it. Filtering these at the call site keeps the period routine's precondition - its input is irrational - explicit, and avoids the division-by-zero the recurrence would otherwise hit.

Step 2 - Iterate the recurrence. Starting from m = 0, d = 1, a = a0, I repeatedly apply three integer updates: m = d*a - m, then d = (N - m*m) / d, then a = (a0 + m) / d. It is a provable fact about this periodic continued fraction recurrence that d always divides N - m^2 exactly, so every quantity stays an integer - no floating point appears inside the loop, and there is no rounding error to reason about.

Step 3 - Detect the period. I accumulate the visited triples. When a freshly computed triple already appears in the collection, the cycle has closed, and the period is the count of triples gathered before that repeat. I test against every stored triple rather than only the initial state. In practice the cycle here always begins immediately at the first generated triple, but the general membership check costs nothing in clarity and is the robust, correct cycle-detection condition.

Step 4 - Count odd periods. The outer loop runs over every non-square N from 2 to the limit, calls the period routine, and tallies each odd result.

Language differences. The Python version stores triples in a list of tuples and tests membership with the in operator - O(period) per check, which is negligible because periods are tiny. It uses math.isqrt for an exact integer square root inside the routine, and filters squares with not math.sqrt(n).is_integer(). The C version stores triples in a dynamically allocated array of int[3], starting at capacity 64 and doubling via realloc. Because C's sqrt operates on double, I wrap each call in two corrective while loops to guarantee an exact integer floor. Both produce 1322 for N up to 10,000; the C build runs in about 0.0062 s against 0.0940 s for Python, roughly a 15x speedup.

workbench

Test cases

categoryinputanswer
dev max_limit = 13 4
main max_limit = 10000 1322

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0064_s0 dev 4 correct 0.000001 (63) 0.000007 (63)
p0064_s0 main 1322 correct 0.006692 (63) 0.107251 (63)

Files

Tags & blogs

Blogs