Problem 57 — Square Root Convergents

solved Thu, 12 Dec 2019, 13:18 · level 1 · difficulty 5% projecteuler.net github

It is possible to show that the square root of two can be expressed as an infinite continued fraction.

$\sqrt 2 =1+ \frac 1 {2+ \frac 1 {2 +\frac 1 {2+ \dots}}}$

By expanding this for the first four iterations, we get:

$1 + \frac 1 2 = \frac 32 = 1.5$
$1 + \frac 1 {2 + \frac 1 2} = \frac 7 5 = 1.4$
$1 + \frac 1 {2 + \frac 1 {2+\frac 1 2}} = \frac {17}{12} = 1.41666 \dots$
$1 + \frac 1 {2 + \frac 1 {2+\frac 1 {2+\frac 1 2}}} = \frac {41}{29} = 1.41379 \dots$

The next three expansions are $\frac {99}{70}$, $\frac {239}{169}$, and $\frac {577}{408}$, but the eighth expansion, $\frac {1393}{985}$, is the first example where the number of digits in the numerator exceeds the number of digits in the denominator.

In the first one-thousand expansions, how many fractions contain a numerator with more digits than the denominator?


Problem Analysis

The number sqrt(2) admits a representation as an infinite continued fraction - an expression formed by repeatedly nesting a term of the shape 1/(2 + 1/(2 + ...)). Truncating that infinite nesting after a finite number of levels yields a rational approximation called a convergent. The task is to count, among the first thousand convergents, how many have a numerator whose number of decimal digits strictly exceeds that of the denominator.

The observation that makes this tractable is that consecutive convergents are linked by a clean two-variable recurrence relation. If the current convergent is p/q, the next one is (p + 2q)/(p + q). This follows directly from the structure of the expansion: adding one more level wraps the previous fraction as 1 + 1/(1 + p/q), and that algebraic substitution sends the pair (p, q) to (p + 2q, p + q). The fractions in the problem - 3/2, 7/5, 17/12, 41/29, 99/70, ... - all satisfy this map exactly. Because the recurrence is built from nothing but addition, I can generate every convergent with exact integer arithmetic, never touching division or floating point. The price is growth: p and q grow by roughly a factor of 1 + sqrt(2) per step, so after n steps they carry on the order of n digits and require arbitrary-precision integers ("bignums"). Each step is dominated by adding two D-digit numbers where D grows linearly with the index, so the total cost is the sum 1 + 2 + ... + n, giving overall complexity of O(n^2).

Solution Approach

Iterative Convergent Recurrence with Big Integers

Both the Python and C solutions implement the same algorithm: hold the current numerator and denominator as arbitrary-precision integers, apply the map (p, q) -> (p + 2q, p + q) once per expansion, and increment a counter on every step where p has more decimal digits than q.

I seed both variables at 1. This represents the state just before the first expansion: applying the recurrence relation once gives numerator 1 + 2*1 = 3 and denominator 1 + 1 = 2, that is, the fraction 3/2 - exactly the problem's first expansion. The critical subtlety is that the two updates must be simultaneous: the new numerator depends on the *old* denominator, and the new denominator depends on the *old* numerator. I must compute both new values before overwriting either, or I corrupt the second update with the first.

In Python, arbitrary-precision integers are built into the language, so the loop body is a single tuple assignment numerator, denominator = (numerator + 2 * denominator, numerator + denominator). To compare magnitudes I convert each integer to its decimal string and compare lengths. That is O(D) per comparison but trivially correct. I also guard the comparison against Python 3.11+'s cap on int-to-string conversion, lifting the limit with sys.set_int_max_str_digits(0) only if the ValueError actually fires.

In C there is no native bignum, so I built a small base-10^9 big-integer type: a fixed-size array of 32-bit limbs stored little-endian (least-significant limb at index 0), each limb holding up to nine decimal digits. Two helpers do the arithmetic - bi_add_twice computes b + 2c (the numerator update) and bi_add computes b + c (the denominator update), both propagating carries across limbs. The choice of base 10^9 pays off in bi_decimal_digits: every limb except the topmost contributes exactly nine digits, so the digit count is 9*(size - 1) plus a short loop over the most-significant limb. I preallocated MAX_LIMBS = 512 slots - far more than the roughly 50 limbs needed even at 10000 expansions - so the hot loop performs no heap allocation.

workbench

Test cases

categoryinputanswer
dev expansions = 10 1
dev expansions = 100 15
main expansions = 1000 153
extra expansions = 10000 1508

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0057_s0 dev 1 correct 0.000001 (63) 0.000002 (63)
p0057_s0 dev 15 correct 0.000006 (63) 0.000025 (63)
p0057_s0 main 153 correct 0.000137 (63) 0.001536 (63)
p0057_s0 extra 1508 correct 0.010102 (63) 0.986285 (63)

Files

Tags & blogs

Blogs