Problem 69 — Totient Maximum

solved Wed, 16 Jul 2025, 11:03 · level 2 · difficulty 6% projecteuler.net github

Euler's totient function, $\phi(n)$ [sometimes called the phi function], is defined as the number of positive integers not exceeding $n$ which are relatively prime to $n$. For example, as $1$, $2$, $4$, $5$, $7$, and $8$, are all less than or equal to nine and relatively prime to nine, $\phi(9)=6$.

$n$ Relatively Prime $\phi(n)$ $n/\phi(n)$
2 1 1 2
3 1,2 2 1.5
4 1,3 2 2
5 1,2,3,4 4 1.25
6 1,5 2 3
7 1,2,3,4,5,6 6 1.1666...
8 1,3,5,7 4 2
9 1,2,4,5,7,8 6 1.5
10 1,3,7,9 4 2.5

It can be seen that $n = 6$ produces a maximum $n/\phi(n)$ for $n\leq 10$.

Find the value of $n\leq 1\,000\,000$ for which $n/\phi(n)$ is a maximum.


Problem Analysis

The engine of this problem is Euler's totient function, written phi(n), which counts the positive integers up to n that are coprime to n - that is, share no common divisor with n greater than 1. The function has a clean closed form built from n's distinct prime factors. If n's distinct primes are p1, p2, ..., pk (their exponents do not matter here), then phi(n) = n * (1 - 1/p1) * (1 - 1/p2) * ... * (1 - 1/pk). The exponents drop out of the product of fractions entirely - only the *set* of distinct primes dividing n appears.

We want to maximise the ratio n/phi(n). Dividing the formula above through by phi gives n/phi(n) = product over distinct primes p dividing n of p/(p-1). This factorisation is the whole game. Every factor p/(p-1) is strictly greater than 1, so adding any new distinct prime strictly *increases* the ratio. Repeating a prime adds nothing to the ratio (only distinct primes count) while still inflating n, so we never raise a prime above the first power. And the factor p/(p-1) is largest for the smallest primes - 2/1 = 2, then 3/2 = 1.5, then 5/4 = 1.25 - shrinking towards 1 as p grows. To extract the most ratio per unit of n, we therefore multiply together consecutive primes starting at 2. The maximiser is always a primorial: the product of the first k primes, for the largest k whose product fits under the bound. This collapses an apparent million-element search into roughly seven multiplications, since 2*3*5*7*11*13*17*19*23 already exceeds a billion. The work is effectively O(1) in the bound.

Solution Approach

Greedy Primorial Accumulation

Both the C and Python files implement the same idea: keep a running product, start it at 1, and multiply in successive primes for as long as the product stays within the limit. The moment the next prime would push us over, we stop, and the last in-bounds product is the answer. Given the analysis above, this greedy march is provably optimal - because the smallest primes give the biggest factors and each distinct prime helps, the dense-as-possible primorial under the bound is the unique maximiser.

The two versions differ only in how they source primes and how they handle the boundary. The C solution uses trial division: for each candidate it tests divisibility by odd numbers up to its square root. Because we need fewer than ten primes, this inner loop is trivially cheap and needs no array allocation. Crucially, the C code follows a *validate-then-commit* discipline. It computes the tentative product in a local next = result * candidate and only writes it back into result when next is confirmed to be at or below the limit. If it overshoots, we break immediately with result untouched. There is no rollback to get wrong.

The Python solution sources its primes from primes_generator, a lazy infinite generator built around an incremental Sieve of Eratosthenes. Instead of a fixed boolean array, it keeps a dictionary composites mapping each "next composite to be struck" back to the prime that strikes it. When a candidate odd n is absent from the dictionary it is prime, so we yield it and record n*n as its first future composite; when n is present it is composite, so we advance that prime's marker by 2*p to the next unoccupied slot. The accumulation loop then multiplies primes into result and, unlike the C version, *commits then checks*: it multiplies first, detects an overshoot, and divides the last prime back out before breaking. Both styles reach the answer in a handful of iterations.

workbench

Test cases

categoryinputanswer
dev limit = 10 6
dev limit = 100 30
dev limit = 1000 210
dev limit = 10000 2310
dev limit = 100000 30030
main limit = 1000000 510510

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0069_s0 dev 6 correct 0.000001 (63) 0.000001 (63)
p0069_s0 dev 30 correct 0.000001 (63) 0.000001 (63)
p0069_s0 dev 210 correct 0.000001 (63) 0.000001 (63)
p0069_s0 dev 2310 correct 0.000001 (63) 0.000002 (63)
p0069_s0 dev 30030 correct 0.000001 (63) 0.000002 (63)
p0069_s0 main 510510 correct 0.000001 (63) 0.000004 (63)

Files

Tags & blogs

Blogs