Problem 410: Circle and Tangent Line
View on Project EulerProject Euler Problem 410 Solution
EulerSolve provides an optimized solution for Project Euler Problem 410, Circle and Tangent Line, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary The geometric statement can be converted into an arithmetic counting problem. For a fixed bound pair \((A,B)\), every admissible tangent configuration is encoded by a positive integer \(n \le A\), a positive integer \(b \le B\), and a factor pair of the square \(b^2\). The direct formulation is still too slow, but it exposes enough structure to turn the geometry into a divisor sum. The official task requires two such counts, for \((10^8,10^9)\) and \((10^9,10^8)\), and adds them together. The whole challenge is therefore to derive a fast closed form for a single subproblem \(F(A,B)\). Mathematical Approach Step 1: Arithmetic form of the tangent condition The direct counting model shows that \(F(A,B)\) can be written in terms of positive integers satisfying $$1 \le n \le A,\qquad 1 \le b \le B,\qquad cd=b^2,$$ together with the two arithmetic constraints $$b \mid n(c+d),\qquad \frac{n(c+d)}{b}\equiv d-c \pmod 2.$$ Whenever these conditions hold, there are two symmetric sign choices, so each admissible arithmetic encoding contributes exactly \(2\) geometric configurations. This is the only part where the geometry still appears; from here onward the problem is pure number theory. Step 2: Every factor pair of a square has a coprime-square core Let \(\lambda=\gcd(c,d)\). Then \(c=\lambda c_0\), \(d=\lambda d_0\), and \(\gcd(c_0,d_0)=1\)....
Detailed mathematical approach
Problem Summary
Let \(C\) be the circle \(x^2+y^2=r^2\) of radius \(r\). We choose two points \(P(a,b)\) and \(Q(-a,c)\) so that the line through \(P\) and \(Q\) is tangent to \(C\). The statement's example is \((r,a,b,c)=(2,6,2,-7)\): the line through \((6,2)\) and \((-6,-7)\) touches the circle of radius \(2\). Let \(F(R,X)\) be the number of integer quadruplets \((r,a,b,c)\) with this property and with \(0\lt r\le R\) and \(0\lt a\le X\); the heights \(b\) and \(c\) are unrestricted integers, positive, negative or zero. We must find \(F(10^8,10^9)+F(10^9,10^8)\). The statement gives the checkpoints \(F(1,5)=10\), \(F(2,10)=52\) and \(F(10,100)=3384\). [1]
The whole solution in one sentence: tangency forces \(a^2\) to split into two factors, every splitting reduces to a pair of coprime squares, and the only conditions that survive are one divisibility and one parity test, which turn \(F(R,X)\) into a single weighted sum of products of two floor divisions.
Mathematical Approach
1. Tangency as one equation
Because \(a\gt0\), the points \(P\) and \(Q\) have different abscissae and always determine one non-vertical line, which is tangent to \(C\) exactly when its distance from the centre \((0,0)\) is \(r\). For the line through \((x_1,y_1)\) and \((x_2,y_2)\) that distance is [2]
$$\frac{|x_2y_1-y_2x_1|}{\sqrt{(y_2-y_1)^2+(x_2-x_1)^2}}.$$
With \((x_1,y_1)=(a,b)\) and \((x_2,y_2)=(-a,c)\) the numerator is \(|{-ab}-ca|=a|b+c|\) and the radicand is \((c-b)^2+4a^2\). Tangency therefore says \(a|b+c|=r\sqrt{(c-b)^2+4a^2}\), and squaring both sides, which are non-negative, gives the equation we use throughout:
$$a^2(b+c)^2=r^2\bigl((c-b)^2+4a^2\bigr).$$
The statement's example fits: \(36\cdot(-5)^2=900\) on the left and \(4\cdot(81+144)=900\) on the right.
2. Sum and difference coordinates
Put \(s=b+c\) and \(d=c-b\). The pair \((b,c)\) can be recovered as \(b=(s-d)/2\) and \(c=(s+d)/2\), so \((b,c)\mapsto(s,d)\) is a bijection from integer pairs onto the integer pairs with \(s\equiv d\pmod 2\). In these coordinates the equation is
$$a^2s^2=r^2\bigl(d^2+4a^2\bigr).$$
3. A hidden factorisation of \(a^2\)
Define \(w=a|s|/r\). Dividing the equation of Section 2 by \(r^2\) gives \(w^2=d^2+4a^2\), a positive integer. A non-negative rational whose square is an integer is itself an integer: writing \(w=p/q\) in lowest terms, \(p^2=w^2q^2\) shows that \(q^2\) divides \(p^2\), while \(\gcd(p,q)=1\) forces \(\gcd(p^2,q^2)=1\), so \(q=1\). (This is the rational root theorem for a monic quadratic.) [3] Moreover \(w^2=d^2+4a^2\gt d^2\), hence \(w\gt|d|\ge0\), so \(w\ge1\) and \(s\ne0\). Finally \(w^2-d^2=4a^2\) is divisible by \(4\), which is impossible if \(w\) and \(d\) have different parities; so \(w\equiv d\pmod 2\) and
$$u=\frac{w-d}{2},\qquad v=\frac{w+d}{2}$$
are positive integers with
$$\begin{aligned}uv&=\frac{w^2-d^2}{4}=a^2,\\v-u&=d,\\u+v&=w.\end{aligned}$$
Conversely, start from an ordered pair \((u,v)\) of positive integers with \(uv=a^2\) and a sign \(\varepsilon=\pm1\), and put \(d=v-u\), \(w=u+v\) and \(s=\varepsilon\,rw/a\). Then
$$\begin{aligned}a^2s^2&=r^2w^2\\&=r^2\bigl((v-u)^2+4uv\bigr)\\&=r^2\bigl(d^2+4a^2\bigr),\end{aligned}$$
so the equation of Section 2 holds automatically; only the integrality of \(s\), \(b\) and \(c\) has to be checked.
Claim. For fixed \(r\) and \(a\), the quadruplets counted by \(F\) correspond one to one with the pairs \(\bigl((u,v),\varepsilon\bigr)\) in which \(u,v\) are positive integers with \(uv=a^2\), the sign \(\varepsilon\) is \(\pm1\), and the two conditions
$$\begin{aligned}&a\mid r(u+v),\\&\frac{r(u+v)}{a}\equiv v-u \pmod 2\end{aligned}$$
hold. The first condition makes \(s\) an integer and the second makes \(b\) and \(c\) integers. Neither mentions \(\varepsilon\), so an admissible splitting yields exactly \(2\) quadruplets.
4. Worked example: the nine splittings of \(36\)
Take \(a=6\), so \(a^2=36\). The nine ordered pairs \((u,v)\) with \(uv=36\), each with \(w=u+v\) and \(d=v-u\), are
- \((1,36)\) and \((36,1)\): \(w=37\), \(d=\pm35\);
- \((2,18)\) and \((18,2)\): \(w=20\), \(d=\pm16\);
- \((3,12)\) and \((12,3)\): \(w=15\), \(d=\pm9\);
- \((4,9)\) and \((9,4)\): \(w=13\), \(d=\pm5\);
- \((6,6)\): \(w=12\), \(d=0\).
Let \(r=2\). The first condition \(6\mid 2w\) means \(3\mid w\), which holds only for \(w=15\) and \(w=12\). For \((3,12)\) we get \(rw/a=5\) and \(d=9\), whose difference \(-4\) is even; for \((6,6)\) we get \(rw/a=4\) and \(d=0\). So three splittings survive, each with two signs, giving six quadruplets with \(r=2\) and \(a=6\): from \((3,12)\) the pairs \((b,c)=(-2,7)\) and \((-7,2)\), from \((6,6)\) the pairs \((2,2)\) and \((-2,-2)\), and from \((12,3)\) the pairs \((7,-2)\) and \((2,-7)\). The last one is the statement's example.
5. Every splitting has a coprime core
Let \(g=\gcd(u,v)\) and write \(u=gu_0\), \(v=gv_0\) with \(\gcd(u_0,v_0)=1\). From \(g^2u_0v_0=a^2\) we get \(u_0v_0=(a/g)^2\), where \(a/g\), a rational with integral square, is again an integer. [3] Now \(u_0v_0\) is a perfect square whose two factors are coprime, so by unique factorisation every prime occurs in exactly one of \(u_0,v_0\), and there to an even exponent. [4] Hence \(u_0=x^2\) and \(v_0=y^2\) for coprime positive integers \(x,y\), and
$$u=gx^2,\quad v=gy^2,\quad a=gxy.$$
Conversely every triple \((g,x,y)\) with \(\gcd(x,y)=1\) and \(gxy=a\) produces a splitting, and the two constructions invert each other, since \(g=\gcd(u,v)\), \(x=\sqrt{u/g}\) and \(y=\sqrt{v/g}\). In the new parameters
$$w=g(x^2+y^2),\qquad d=g(y^2-x^2).$$
For \(a=6\) the splitting \((3,12)\) has \(g=3\), \(x=1\), \(y=2\), while \((4,9)\) has \(g=1\), \(x=2\), \(y=3\), and \((6,6)\) has \(g=6\), \(x=y=1\).
6. The divisibility condition becomes \(m\mid r\)
Write \(m=xy\), so that \(a=gm\). The first condition of Section 3, \(a\mid rw\), reads \(gm\mid rg(x^2+y^2)\), that is \(m\mid r(x^2+y^2)\). The two factors are coprime: if a prime \(\ell\) divides \(m=xy\) then it divides \(x\) or \(y\), say \(x\); if \(\ell\) also divided \(x^2+y^2\) it would divide \(y^2\) and hence \(y\), contradicting \(\gcd(x,y)=1\). So \(\gcd(m,x^2+y^2)=1\) and the condition reads
$$m\mid r.$$
Since \(a=gm\) we also have \(m\mid a\), so a splitting can contribute only when \(m\) divides \(\gcd(r,a)\). Writing \(r=m\rho\), the value \(s\) of Section 3 has absolute value \(rw/a=\rho(x^2+y^2)\).
7. The parity condition
With \(\rho=r/m\) and \(g=a/m\) the second condition of Section 3 compares the two quantities \(\rho(x^2+y^2)\) and \(g(y^2-x^2)\) modulo \(2\). Since \(z^2\equiv z\pmod 2\) for every integer, both \(x^2+y^2\) and \(y^2-x^2\) are congruent to \(x+y\). If \(m=xy\) is odd then \(x\) and \(y\) are both odd, \(x+y\) is even, both quantities are even and the condition holds for every \(\rho\) and \(g\). If \(m\) is even then exactly one of the coprime numbers \(x,y\) is even, \(x+y\) is odd, and the condition becomes
$$\frac{r}{m}\equiv\frac{a}{m}\pmod 2,$$
which is the same as \(2m\mid r-a\). In both cases only the parity of \(m\) matters, not \(x\) and \(y\) themselves.
8. How many quadruplets belong to one pair \((r,a)\)
Fix \(m\). The number of ordered pairs \((x,y)\) with \(xy=m\) and \(\gcd(x,y)=1\) is \(2^{\omega(m)}\), where \(\omega(m)\) counts the distinct prime divisors of \(m\): [5] by unique factorisation each of the \(\omega(m)\) prime powers in \(m\) must go entirely to \(x\) or entirely to \(y\). Equivalently, \(x\) runs through the unitary divisors of \(m\). [6] Each pair fixes \(g=a/m\) and hence one splitting, worth \(2\) quadruplets. Combining Sections 3 to 7:
Claim. For fixed \(r,a\ge1\), the number \(G(r,a)\) of integer pairs \((b,c)\) making the line through \((a,b)\) and \((-a,c)\) tangent to \(x^2+y^2=r^2\) is
$$G(r,a)=2\sum_{m\in D(r,a)}2^{\omega(m)},$$
where \(D(r,a)\) consists of those divisors \(m\) of \(\gcd(r,a)\) that are odd, together with those even ones for which \(2m\) divides \(r-a\).
Two instances. For \(r=2\), \(a=6\): \(\gcd=2\), the divisor \(m=1\) is odd and contributes \(1\), and \(m=2\) is even with \(2m=4\) dividing \(r-a=-4\), contributing \(2\); so \(G(2,6)=2(1+2)=6\), exactly the six quadruplets listed in Section 4. For \(r=1\) only \(m=1\) is available, so \(G(1,a)=2\) for every \(a\) — the two horizontal tangents \(y=1\) and \(y=-1\), realised by \(b=c=1\) and \(b=c=-1\). This already gives the first checkpoint, \(F(1,5)=2\cdot5=10\).
A check on the weights: dropping both conditions, \(\sum_{m\mid a}2^{\omega(m)}\) must count all ordered splittings of \(a^2\), that is the number of divisors \(d(a^2)\). For \(a=6\) this reads \(1+2+2+4=9=d(36)\).
9. Summing over the rectangle
Now sum \(G\) over the rectangle and exchange the order of summation: instead of asking which \(m\) suit a given \((r,a)\), ask which \((r,a)\) suit a given \(m\). Write
$$t=\Bigl\lfloor\frac{R}{m}\Bigr\rfloor,\qquad h=\Bigl\lfloor\frac{X}{m}\Bigr\rfloor,$$
so that the multiples of \(m\) in range are \(r=m\rho\) with \(1\le\rho\le t\) and \(a=mg\) with \(1\le g\le h\). For odd \(m\) all \(th\) pairs are admissible. For even \(m\) only those with \(\rho\equiv g\pmod 2\) are, and their number is
$$\Bigl\lceil\frac t2\Bigr\rceil\Bigl\lceil\frac h2\Bigr\rceil+\Bigl\lfloor\frac t2\Bigr\rfloor\Bigl\lfloor\frac h2\Bigr\rfloor=\frac{th+\tau\eta}{2},$$
where \(\tau\) and \(\eta\) are the parities of \(t\) and \(h\); the identity follows from \(\lceil t/2\rceil=(t+\tau)/2\) and \(\lfloor t/2\rfloor=(t-\tau)/2\) by expanding both products. Since \(m\mid\gcd(r,a)\) forces \(m\le\min(R,X)\), we obtain the formula the programs evaluate:
$$F(R,X)=\sum_{m=1}^{\min(R,X)}2^{\omega(m)}\,U_m,$$
with \(U_m=2th\) when \(m\) is odd and \(U_m=th+\tau\eta\) when \(m\) is even. The factor \(2\) of the two signs has been absorbed into \(U_m\), which is why the even case loses the division by \(2\).
10. Worked example: \(F(10,100)=3384\)
Here \(\min(R,X)=10\), so there are ten terms. Each line below gives \(m\), the weight \(2^{\omega(m)}\), then \(t\), \(h\), \(U_m\) and the term. No even \(m\) here has both \(t\) and \(h\) odd, so the correction \(\tau\eta\) vanishes throughout.
- \(m=1\): \(1\), \(t=10\), \(h=100\), \(U=2000\), term \(2000\);
- \(m=2\): \(2\), \(t=5\), \(h=50\), \(U=250\), term \(500\);
- \(m=3\): \(2\), \(t=3\), \(h=33\), \(U=198\), term \(396\);
- \(m=4\): \(2\), \(t=2\), \(h=25\), \(U=50\), term \(100\);
- \(m=5\): \(2\), \(t=2\), \(h=20\), \(U=80\), term \(160\);
- \(m=6\): \(4\), \(t=1\), \(h=16\), \(U=16\), term \(64\);
- \(m=7\): \(2\), \(t=1\), \(h=14\), \(U=28\), term \(56\);
- \(m=8\): \(2\), \(t=1\), \(h=12\), \(U=12\), term \(24\);
- \(m=9\): \(2\), \(t=1\), \(h=11\), \(U=22\), term \(44\);
- \(m=10\): \(4\), \(t=1\), \(h=10\), \(U=10\), term \(40\).
The ten terms add up to \(3384\), the third checkpoint. The second shows the parity correction at work: for \(F(2,10)\), \(m=1\) gives \(1\cdot2\cdot2\cdot10=40\), and \(m=2\) has \(t=1\) and \(h=5\), both odd, so \(U_2=1\cdot5+1=6\) and the term is \(2\cdot6=12\); the total is \(40+12=52\). Without the correction \(\tau\eta\) the second term would be \(10\) and the total \(50\).
11. A symmetry and the size of the result
The quantity \(U_m\) is symmetric in \(t\) and \(h\), so \(F(R,X)=F(X,R)\) for all bounds: the requested total is twice one evaluation, although the geometry gives \(r\) and \(a\) quite different roles. For the size, \(U_m\le 2th\le 2RX/m^2\), so we need the sum of \(2^{\omega(m)}/m^2\). For real \(s\gt1\) let \(\zeta(s)=\sum_{n\ge1}n^{-s}\) be the Riemann zeta function, which unique factorisation turns into the Euler product \(\prod_p(1-p^{-s})^{-1}\). [6] In our sum every positive power of a prime \(p\) carries the weight \(2\), so the factor of \(p\) is
$$\begin{aligned}1+2\sum_{k\ge1}p^{-ks}&=\frac{1+p^{-s}}{1-p^{-s}}\\&=\frac{1-p^{-2s}}{(1-p^{-s})^2},\end{aligned}$$
and multiplying these factors over all primes gives the Dirichlet series of the weights,
$$\sum_{m\ge1}\frac{2^{\omega(m)}}{m^s}=\frac{\zeta(s)^2}{\zeta(2s)}.$$
At \(s=2\), where \(\zeta(2)=\pi^2/6\) and \(\zeta(4)=\pi^4/90\), this is exactly \(\tfrac52\), [6] so
$$F(R,X)\le 2RX\cdot\tfrac52=5RX,$$
and both values, and their sum, therefore stay comfortably inside the signed 64-bit range.
How the Code Works
All three programs evaluate the formula of Section 9 directly; the only preparation is a table of \(\omega\). The names below are the C++ ones; Java spells them buildDistinctPrimeCounts and solveF, called from solve, while the Python solve builds the sieve inline and defines solve_F as a nested function over the enclosing omega. [9]
build_distinct_prime_counts(limit) builds that table in three passes. First it marks the odd composites: a bit vector indexed by p >> 1, that is by \(\lfloor p/2\rfloor\), holds one flag per odd number, and for every still unmarked odd \(p\) up to the square root of the limit it marks \(p^2,\,p^2+2p,\,p^2+4p,\dots\), which is the sieve of Eratosthenes on the odd numbers. [7] Second it adds \(1\) to \(\omega(m)\) for every even \(m\), the contribution of the prime \(2\). Third, for every unmarked odd \(p\) it adds \(1\) to \(\omega(m)\) for all multiples \(m\) of \(p\). The entry \(\omega(1)\) stays \(0\), the correct weight \(1\), and the table holds bytes because \(\omega(m)\le8\) for \(m\le10^8\): the product of the first eight primes is \(9\,699\,690\), while including \(23\) gives \(223\,092\,870\).
solve_F_with_omega(R, X, omega, …) is the loop of Section 9. It sets limit = min(R, X) and for each \(m\) reads t = R / m, g = X / m and weight = 1ULL << omega[m], then forms unit as 2 * t * g for odd \(m\) and as t * g + ((t & 1) & (g & 1)) for even \(m\) — the \(th+\tau\eta\) of Section 9, the bitwise and of the low bits being \(\tau\eta\) — and accumulates weight * unit. solve_F is the same routine with the sieve built on the spot, which is how the checkpoints call it.
main builds one table for the larger of the two values of \(\min(R,X)\) (both are \(10^8\) here), evaluates the first \(F\), and reuses that value for the second pair when the second pair is the first one swapped, which is the symmetry of Section 11.
brute_force_reference(R, X) is the slow reference. For every \(r\le R\) and \(a\le X\) it tries every \(u\) from \(1\) to \(a^2\), keeps the divisors of \(a^2\), sets \(v=a^2/u\), \(d=v-u\), \(k=u+v\), and applies the two tests of Section 3, namely \(a\mid rk\) and the parity of \(rk/a-d\), adding \(2\) for the two signs. It thus enumerates the bijection of Section 3 one splitting at a time, testing the regrouping of Sections 5 to 9 but not the geometry.
run_checkpoints compares the fast routine with the statement's three values, then checks that brute_force_reference(12, 20) gives \(824\) and that the fast routine agrees, and under the default options evaluates \(F(200\,000,300\,000)\) twice, accumulating the terms in two different orders and requiring the two results to agree. The C++ command line can override the four bounds and skip the checkpoints, which produced the smaller values quoted here; the Python and Java programs use fixed bounds and carry no checks of their own.
The implementations also differ in arithmetic and bookkeeping. The C++ accumulates in unsigned __int128 and prints through a hand-written decimal conversion; Java emulates that width with a sumHi/sumLo pair, detecting carries with Long.compareUnsigned, and converts to BigInteger at the end; Python uses its unbounded integers. By Section 11 even 64 bits would suffice, and no single product weight * unit exceeds \(2RX\), since \(2^{\omega(m)}/m^2\le1\) for every \(m\). Java keeps its odd-composite flags in a byte[] rather than a bit vector, eight times the memory, and Python mirrors the sieve with two bytearrays and has no symmetry shortcut, so it evaluates the \(10^8\)-term sum twice.
Complexity and Verification
Let \(L=\min(R,X)\), which is \(10^8\) for both requested pairs. Marking the odd composites costs \(O(L\log\log L)\) operations [7] and performs \(96\,285\,414\) writes at \(L=10^8\). The two counting passes increment once for each pair of a prime \(p\le L\) and a multiple of \(p\), that is \(\sum_{p\le L}\lfloor L/p\rfloor\) times, exactly \(315\,037\,281\) here with \(5\cdot10^7\) of them from the prime \(2\); before the quotients are rounded down, Mertens' estimate \(\sum_{p\le L}1/p\approx\log\log L+M\) with \(M\approx0.2615\) predicts about \(3.17\cdot10^8\). [5] The summation itself is \(O(L)\): two floor divisions, a shift and a few multiplications per term. The method is therefore \(O(L\log\log L)\) in time and \(O(L)\) in memory, the sieve tables dominating storage: at \(L=10^8\) about \(106\) MB in C++, namely \(100\) MB for the byte table and \(6.25\) MB for the packed odd flags, and about \(150\) MB in Java and Python, which store the flags one byte each.
One run of each program on the same machine needed about \(0.6\) seconds and \(108\) MB of resident memory for the C++ version, \(0.7\) seconds of wall time and \(200\) MB for Java, and \(40\) seconds and \(164\) MB for Python, the last consistent with the two sums it evaluates instead of one. Resident memory exceeds the table sizes by runtime overhead, largest for Java.
Beyond the packaged checkpoints, separate programs were run for this article. The first goes back to the original equation, over a search interval taken from Section 3: since \(u,v\ge1\) and \(uv=a^2\), the inequality \((u-1)(v-1)\ge0\) gives \(u+v\le a^2+1\), and \(|v-u|\le a^2-1\), so \(|s|\le r(a^2+1)/a\) and \(|d|\le a^2-1\), which bounds \(b=(s-d)/2\). For every \(b\) in that interval the program solves
$$\begin{aligned}(a^2-r^2)c^2&+2b(a^2+r^2)c\\&+(a^2-r^2)b^2-4a^2r^2=0\end{aligned}$$
for \(c\) in exact integer arithmetic, treating the degenerate case \(a=r\) separately, where the condition collapses to \(bc=a^2\). For \(a\ne r\) the discriminant is \(16a^2r^2(a^2+b^2-r^2)\), that is \(16a^2r^2\) times the power \(a^2+b^2-r^2\) of the point \(P\) with respect to \(C\), so an integer root needs an integral tangent length from \(P\) [8] and, beyond that, a numerator divisible by \(2(a^2-r^2)\). Apart from that search interval the check uses nothing from Sections 5 to 9 and no sieve: it solves the tangency equation itself. It agrees with the formula for every \(R,X\le12\), among them \(F(1,5)=10\), \(F(2,10)=52\), \(F(3,7)=58\) and \(F(5,5)=70\), and separately at \(F(12,20)=824\). The remaining checks were these.
- The claim of Section 8: \(G(r,a)\) agrees with the geometric count for every \(r,a\le16\), and summing \(G\) over the rectangle agrees with the closed formula for all \(R,X\le15\).
- The weight: the number of ordered coprime pairs \((x,y)\) with \(xy=m\) equals \(2^{\omega(m)}\) for all \(m\le2000\), and \(\sum_{m\mid a}2^{\omega(m)}=d(a^2)\) for all \(a\le2000\).
- The parity identity of Section 9 for all \(0\le t,h\lt60\), and the partial sum of \(2^{\omega(m)}/m^2\) over \(m\le4000\), which is approximately \(2.498239\) and approaches the limit \(\tfrac52\) of Section 11.
- The symmetry \(F(R,X)=F(X,R)\) for all \(R,X\le30\) and at \((10^3,10^4)\); and the published sieve reproduces \(\omega(m)\) from a factorisation of every \(m\le200\,000\).
- Agreement between the implementations: the C++ program and an independent Python evaluation of the formula both give \(F(10^4,10^5)=3\,996\,520\,500\), and at the requested bounds the three programs' outputs have matching checksums.
Two notes on robustness, not on the result: the sieve takes \(\sqrt{L}\) from a floating-point square root, exact at \(L=10^8\) but able to undershoot by one for far larger limits, which would leave the square of a prime unmarked; and 2 * t * g would overflow 64 bits for bounds beyond a few times \(10^9\). Both are safe here.
Footnotes and References
The references below supply the background facts used above. The bijection between quadruplets and splittings of \(a^2\), the coprime parameters of Section 5, the two arithmetic conditions, the count \(G(r,a)\) and the closed formula are derived explicitly in this article.
- Project Euler 410 — Circle and Tangent Line. The official statement defines F(R, X) as the number of integer quadruplets (r, a, b, c) for which the line through P(a, b) and Q(−a, c) is tangent to the circle x² + y² = r², with 0 < r ≤ R and 0 < a ≤ X, gives the example (2, 6, 2, −7) and the three values quoted in the summary, and asks for F(10⁸, 10⁹) + F(10⁹, 10⁸).
- Wikipedia — Distance from a point to a line. Under "Line defined by two points" the article gives the distance from (x₀, y₀) to the line through (x₁, y₁) and (x₂, y₂) as |(y₂ − y₁)x₀ − (x₂ − x₁)y₀ + x₂y₁ − y₂x₁| over the square root of (y₂ − y₁)² + (x₂ − x₁)². At the origin only x₂y₁ − y₂x₁ survives, which is Section 1.
- Wikipedia — Rational root theorem. A rational root p/q in lowest terms of a polynomial with integer coefficients has q dividing the leading coefficient, and the article states the monic case separately as the integral root theorem. Sections 3 and 5 use the consequence that a rational number whose square is an integer is an integer.
- Wikipedia — Fundamental theorem of arithmetic. Every integer greater than 1 is a product of primes, uniquely up to order, and the article writes each integer in canonical form as a product of prime powers with uniquely determined exponents. Sections 5 and 8 split the exponents of a square between two coprime factors this way.
- Wikipedia — Prime omega function. The article defines ω(n) as the number of distinct prime factors of n, notes that ω is additive and gives log log n as an average order — the quantity that appears, through Mertens' estimate for the sum of reciprocals of primes, in the sieve's operation count.
- OEIS A034444 — number of unitary divisors. The entry counts the divisors d of n with gcd(d, n/d) = 1 and gives both a(n) = 2 raised to ω(n) and the Dirichlet generating function zeta(s)²/zeta(2s), used in Sections 8 and 11. NIST DLMF §25.2 defines the zeta series in equation 25.2.1 and its Euler product over the primes in 25.2.11, both for Re s > 1, and equation 25.6.1 lists ζ(2) = π²/6 and ζ(4) = π⁴/90.
- Wikipedia — Sieve of Eratosthenes. The article marks the multiples of each prime, states that the outer loop need not exceed the square root of the limit, and gives the running time as O(n log log n) when each array update is O(1).
- Wikipedia — Power of a point. The power of P with respect to a circle of centre O and radius r is |PO|² − r²; it is positive exactly when P lies outside the circle and then equals the squared length of the tangent segment from P. It is the factor a² + b² − r² of the discriminant in the geometric check.
- C++ — Euler410.cpp. The linked immutable revision contains
build_distinct_prime_counts,solve_F_with_omega,brute_force_referenceand the checkpoints at F(1, 5), F(2, 10), F(10, 100) and F(12, 20). It is the implementation record for this article; the additional checks listed above are not packaged in it.
Problem 410 source code
C++
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
namespace {
using u64 = std::uint64_t;
using u8 = std::uint8_t;
using u128 = unsigned __int128;
constexpr u64 kDefaultR1 = 100'000'000ULL;
constexpr u64 kDefaultX1 = 1'000'000'000ULL;
constexpr u64 kDefaultR2 = 1'000'000'000ULL;
constexpr u64 kDefaultX2 = 100'000'000ULL;
constexpr u64 kCheckpointR1 = 1ULL;
constexpr u64 kCheckpointX1 = 5ULL;
constexpr u64 kCheckpointExpected1 = 10ULL;
constexpr u64 kCheckpointR2 = 2ULL;
constexpr u64 kCheckpointX2 = 10ULL;
constexpr u64 kCheckpointExpected2 = 52ULL;
constexpr u64 kCheckpointR3 = 10ULL;
constexpr u64 kCheckpointX3 = 100ULL;
constexpr u64 kCheckpointExpected3 = 3384ULL;
constexpr u64 kBruteCheckR = 12ULL;
constexpr u64 kBruteCheckX = 20ULL;
constexpr u64 kBruteCheckExpected = 824ULL;
constexpr u64 kThreadConsistencyR = 200'000ULL;
constexpr u64 kThreadConsistencyX = 300'000ULL;
struct Options {
u64 r1 = kDefaultR1;
u64 x1 = kDefaultX1;
u64 r2 = kDefaultR2;
u64 x2 = kDefaultX2;
bool run_checkpoints = true;
bool allow_multithreading = true;
unsigned requested_threads = 0U;
};
bool parse_u64_after_prefix(const std::string& arg, const char* prefix, u64& value) {
const std::string p(prefix);
if (arg.rfind(p, 0) != 0) {
return false;
}
const std::string tail = arg.substr(p.size());
if (tail.empty()) {
return false;
}
u64 parsed = 0ULL;
for (const char c : tail) {
if (c < '0' || c > '9') {
return false;
}
const u64 digit = static_cast<u64>(c - '0');
if (parsed > (std::numeric_limits<u64>::max() - digit) / 10ULL) {
return false;
}
parsed = parsed * 10ULL + digit;
}
value = parsed;
return true;
}
bool parse_unsigned_after_prefix(const std::string& arg,
const char* prefix,
unsigned& value) {
u64 parsed = 0ULL;
if (!parse_u64_after_prefix(arg, prefix, parsed)) {
return false;
}
if (parsed > static_cast<u64>(std::numeric_limits<unsigned>::max())) {
return false;
}
value = static_cast<unsigned>(parsed);
return true;
}
bool parse_arguments(int argc, char** argv, Options& options) {
for (int i = 1; i < argc; ++i) {
const std::string arg(argv[i]);
if (arg == "--skip-checkpoints") {
options.run_checkpoints = false;
continue;
}
if (arg == "--single-thread") {
options.allow_multithreading = false;
continue;
}
u64 parsed_u64 = 0ULL;
if (parse_u64_after_prefix(arg, "--r1=", parsed_u64)) {
options.r1 = parsed_u64;
continue;
}
if (parse_u64_after_prefix(arg, "--x1=", parsed_u64)) {
options.x1 = parsed_u64;
continue;
}
if (parse_u64_after_prefix(arg, "--r2=", parsed_u64)) {
options.r2 = parsed_u64;
continue;
}
if (parse_u64_after_prefix(arg, "--x2=", parsed_u64)) {
options.x2 = parsed_u64;
continue;
}
unsigned parsed_unsigned = 0U;
if (parse_unsigned_after_prefix(arg, "--threads=", parsed_unsigned)) {
options.requested_threads = parsed_unsigned;
continue;
}
std::cerr << "Unknown argument: " << arg << '\n';
return false;
}
if (options.r1 == 0ULL || options.x1 == 0ULL || options.r2 == 0ULL || options.x2 == 0ULL) {
std::cerr << "All bounds must be at least 1.\n";
return false;
}
return true;
}
unsigned choose_thread_count(bool allow_multithreading,
unsigned requested_threads,
std::size_t workload) {
if (!allow_multithreading || workload < 2ULL) {
return 1U;
}
unsigned threads = requested_threads;
if (threads == 0U) {
threads = std::thread::hardware_concurrency();
if (threads == 0U) {
threads = 1U;
}
}
return std::max(1U, std::min<unsigned>(threads, static_cast<unsigned>(workload)));
}
std::string to_string_u128(u128 value) {
if (value == 0) {
return "0";
}
std::string s;
while (value > 0) {
const unsigned digit = static_cast<unsigned>(value % 10U);
s.push_back(static_cast<char>('0' + digit));
value /= 10U;
}
std::reverse(s.begin(), s.end());
return s;
}
std::vector<u8> build_distinct_prime_counts(u64 limit) {
std::vector<u8> omega(static_cast<std::size_t>(limit) + 1ULL, 0U);
if (limit < 2ULL) {
return omega;
}
std::vector<bool> composite((static_cast<std::size_t>(limit) >> 1ULL) + 1ULL, false);
const u64 root = static_cast<u64>(std::sqrt(static_cast<long double>(limit)));
for (u64 p = 3ULL; p <= root; p += 2ULL) {
if (composite[static_cast<std::size_t>(p >> 1ULL)]) {
continue;
}
const u64 step = p << 1ULL;
for (u64 j = p * p; j <= limit; j += step) {
composite[static_cast<std::size_t>(j >> 1ULL)] = true;
}
}
for (u64 m = 2ULL; m <= limit; m += 2ULL) {
++omega[static_cast<std::size_t>(m)];
}
for (u64 p = 3ULL; p <= limit; p += 2ULL) {
if (composite[static_cast<std::size_t>(p >> 1ULL)]) {
continue;
}
for (u64 m = p; m <= limit; m += p) {
++omega[static_cast<std::size_t>(m)];
}
}
return omega;
}
u64 brute_force_reference(u64 R, u64 X) {
u64 total = 0ULL;
for (u64 r = 1ULL; r <= R; ++r) {
for (u64 a = 1ULL; a <= X; ++a) {
const u64 a2 = a * a;
for (u64 u = 1ULL; u <= a2; ++u) {
if (a2 % u != 0ULL) {
continue;
}
const u64 v = a2 / u;
const long long d = static_cast<long long>(v) - static_cast<long long>(u);
const u64 k = u + v;
const u64 rk = r * k;
if (rk % a != 0ULL) {
continue;
}
const long long s_abs = static_cast<long long>(rk / a);
if (((s_abs - d) & 1LL) != 0LL) {
continue;
}
total += 2ULL; // s = +s_abs and s = -s_abs
}
}
}
return total;
}
u128 solve_F_with_omega(u64 R,
u64 X,
const std::vector<u8>& omega,
bool allow_multithreading,
unsigned requested_threads) {
const u64 limit = std::min(R, X);
if (limit == 0ULL) {
return 0;
}
const unsigned threads = choose_thread_count(
allow_multithreading, requested_threads, static_cast<std::size_t>(limit));
std::vector<u128> partial(threads, 0);
auto worker = [&](unsigned tid) {
u128 local = 0;
for (u64 m = 1ULL + static_cast<u64>(tid); m <= limit; m += threads) {
const u64 t = R / m;
const u64 g = X / m;
const u64 weight = 1ULL << omega[static_cast<std::size_t>(m)];
u64 unit = 0ULL;
if ((m & 1ULL) != 0ULL) {
unit = 2ULL * t * g;
} else {
unit = t * g + ((t & 1ULL) & (g & 1ULL));
}
local += static_cast<u128>(weight) * static_cast<u128>(unit);
}
partial[tid] = local;
};
std::vector<std::thread> pool;
pool.reserve(threads > 0U ? threads - 1U : 0U);
for (unsigned t = 1U; t < threads; ++t) {
pool.emplace_back(worker, t);
}
worker(0U);
for (std::thread& thread : pool) {
thread.join();
}
u128 total = 0;
for (const u128 value : partial) {
total += value;
}
return total;
}
u128 solve_F(u64 R,
u64 X,
bool allow_multithreading,
unsigned requested_threads) {
const u64 limit = std::min(R, X);
const std::vector<u8> omega = build_distinct_prime_counts(limit);
return solve_F_with_omega(R, X, omega, allow_multithreading, requested_threads);
}
bool run_checkpoints(const Options& options) {
{
const u128 got = solve_F(kCheckpointR1,
kCheckpointX1,
options.allow_multithreading,
options.requested_threads);
if (got != static_cast<u128>(kCheckpointExpected1)) {
std::cerr << "Checkpoint failed: F(" << kCheckpointR1 << ", " << kCheckpointX1
<< ") expected " << kCheckpointExpected1
<< ", got " << to_string_u128(got) << '\n';
return false;
}
std::cout << "Checkpoint passed: F(" << kCheckpointR1 << ", " << kCheckpointX1
<< ") = " << kCheckpointExpected1 << '\n';
}
{
const u128 got = solve_F(kCheckpointR2,
kCheckpointX2,
options.allow_multithreading,
options.requested_threads);
if (got != static_cast<u128>(kCheckpointExpected2)) {
std::cerr << "Checkpoint failed: F(" << kCheckpointR2 << ", " << kCheckpointX2
<< ") expected " << kCheckpointExpected2
<< ", got " << to_string_u128(got) << '\n';
return false;
}
std::cout << "Checkpoint passed: F(" << kCheckpointR2 << ", " << kCheckpointX2
<< ") = " << kCheckpointExpected2 << '\n';
}
{
const u128 got = solve_F(kCheckpointR3,
kCheckpointX3,
options.allow_multithreading,
options.requested_threads);
if (got != static_cast<u128>(kCheckpointExpected3)) {
std::cerr << "Checkpoint failed: F(" << kCheckpointR3 << ", " << kCheckpointX3
<< ") expected " << kCheckpointExpected3
<< ", got " << to_string_u128(got) << '\n';
return false;
}
std::cout << "Checkpoint passed: F(" << kCheckpointR3 << ", " << kCheckpointX3
<< ") = " << kCheckpointExpected3 << '\n';
}
{
const u64 brute = brute_force_reference(kBruteCheckR, kBruteCheckX);
if (brute != kBruteCheckExpected) {
std::cerr << "Internal brute checkpoint mismatch: expected "
<< kBruteCheckExpected << ", got " << brute << '\n';
return false;
}
const u128 fast = solve_F(kBruteCheckR,
kBruteCheckX,
options.allow_multithreading,
options.requested_threads);
if (fast != static_cast<u128>(brute)) {
std::cerr << "Fast-vs-brute checkpoint failed at F(" << kBruteCheckR << ", "
<< kBruteCheckX << "): brute=" << brute
<< ", fast=" << to_string_u128(fast) << '\n';
return false;
}
std::cout << "Checkpoint passed: F(" << kBruteCheckR << ", " << kBruteCheckX
<< ") = " << brute << " (matches brute force)\n";
}
if (options.allow_multithreading) {
const u64 limit = std::min(kThreadConsistencyR, kThreadConsistencyX);
const std::vector<u8> omega = build_distinct_prime_counts(limit);
const u128 multi = solve_F_with_omega(kThreadConsistencyR,
kThreadConsistencyX,
omega,
true,
options.requested_threads);
const u128 single = solve_F_with_omega(kThreadConsistencyR,
kThreadConsistencyX,
omega,
false,
options.requested_threads);
if (multi != single) {
std::cerr << "Thread consistency failed at F(" << kThreadConsistencyR << ", "
<< kThreadConsistencyX << "): single=" << to_string_u128(single)
<< ", multi=" << to_string_u128(multi) << '\n';
return false;
}
std::cout << "Checkpoint passed: thread consistency at F(" << kThreadConsistencyR
<< ", " << kThreadConsistencyX << ")\n";
}
return true;
}
} // namespace
int main(int argc, char** argv) {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
Options options;
if (!parse_arguments(argc, argv, options)) {
return 1;
}
try {
const auto start_time = std::chrono::steady_clock::now();
if (options.run_checkpoints && !run_checkpoints(options)) {
return 1;
}
const u64 limit1 = std::min(options.r1, options.x1);
const u64 limit2 = std::min(options.r2, options.x2);
const u64 max_limit = std::max(limit1, limit2);
const std::vector<u8> omega = build_distinct_prime_counts(max_limit);
const u128 f1 = solve_F_with_omega(options.r1,
options.x1,
omega,
options.allow_multithreading,
options.requested_threads);
u128 f2 = 0;
if (options.r2 == options.x1 && options.x2 == options.r1) {
f2 = f1;
} else {
f2 = solve_F_with_omega(options.r2,
options.x2,
omega,
options.allow_multithreading,
options.requested_threads);
}
const u128 answer = f1 + f2;
const auto end_time = std::chrono::steady_clock::now();
const std::chrono::duration<double> elapsed = end_time - start_time;
std::cout << "F(" << options.r1 << ", " << options.x1 << ") = "
<< to_string_u128(f1) << '\n';
std::cout << "F(" << options.r2 << ", " << options.x2 << ") = "
<< to_string_u128(f2) << '\n';
std::cout << "Answer = " << to_string_u128(answer) << '\n';
std::cout << "Elapsed: " << elapsed.count() << " seconds\n";
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << '\n';
return 1;
}
return 0;
}
Python
def solve():
R1, X1 = 100000000, 1000000000
R2, X2 = 1000000000, 100000000
limit = max(min(R1, X1), min(R2, X2))
# Build distinct prime factor count (omega) via sieve
omega = bytearray(limit + 1)
is_comp = bytearray((limit >> 1) + 1)
root = int(limit**0.5)
for p in range(3, root + 1, 2):
if is_comp[p >> 1]: continue
for j in range(p * p, limit + 1, p << 1):
is_comp[j >> 1] = 1
for m in range(2, limit + 1, 2):
omega[m] += 1
for p in range(3, limit + 1, 2):
if is_comp[p >> 1]: continue
for m in range(p, limit + 1, p):
omega[m] += 1
def solve_F(R, X):
lim = min(R, X)
total = 0
for m in range(1, lim + 1):
t = R // m
g = X // m
w = 1 << omega[m]
if m & 1:
unit = 2 * t * g
else:
unit = t * g + ((t & 1) & (g & 1))
total += w * unit
return total
return str(solve_F(R1, X1) + solve_F(R2, X2))
if __name__ == '__main__':
print(solve())
Java
import java.math.BigInteger;
import java.util.stream.IntStream;
public class Euler410 {
static byte[] buildDistinctPrimeCounts(int limit) {
byte[] omega = new byte[limit + 1];
if (limit < 2)
return omega;
byte[] composite = new byte[(limit >> 1) + 1];
int root = (int) Math.sqrt(limit);
for (int p = 3; p <= root; p += 2) {
if (composite[p >> 1] != 0)
continue;
int step = p << 1;
for (int j = p * p; j <= limit; j += step) {
composite[j >> 1] = 1;
}
}
for (int m = 2; m <= limit; m += 2) {
omega[m]++;
}
for (int p = 3; p <= limit; p += 2) {
if (composite[p >> 1] != 0)
continue;
for (int m = p; m <= limit; m += p) {
omega[m]++;
}
}
return omega;
}
static BigInteger solveF(long R, long X, byte[] omega) {
int limit = (int) Math.min(R, X);
if (limit == 0)
return BigInteger.ZERO;
int numChunks = Runtime.getRuntime().availableProcessors();
long chunkSize = (limit + numChunks - 1) / numChunks;
BigInteger total = IntStream.range(0, numChunks)
.parallel()
.mapToObj(i -> {
int start = (int) (i * chunkSize + 1);
int end = (int) Math.min((i + 1) * chunkSize, limit);
long sumHi = 0;
long sumLo = 0;
for (int m = start; m <= end; m++) {
long t = R / m;
long g = X / m;
long weight = 1L << omega[m];
long unit;
if ((m & 1) != 0) {
unit = 2L * t * g;
} else {
unit = t * g + ((t & 1) & (g & 1));
}
long term = weight * unit;
sumLo += term;
if (Long.compareUnsigned(sumLo, term) < 0) {
sumHi++;
}
}
BigInteger hi = BigInteger.valueOf(sumHi).shiftLeft(64);
BigInteger lo = BigInteger.valueOf(sumLo);
if (sumLo < 0) {
lo = lo.add(BigInteger.ONE.shiftLeft(64));
}
return hi.add(lo);
})
.reduce(BigInteger.ZERO, BigInteger::add);
return total;
}
static String solve() {
long r1 = 100000000L;
long x1 = 1000000000L;
long r2 = 1000000000L;
long x2 = 100000000L;
int maxLimit = (int) Math.max(Math.min(r1, x1), Math.min(r2, x2));
byte[] omega = buildDistinctPrimeCounts(maxLimit);
BigInteger f1 = solveF(r1, x1, omega);
BigInteger f2;
if (r2 == x1 && x2 == r1) {
f2 = f1;
} else {
f2 = solveF(r2, x2, omega);
}
return f1.add(f2).toString();
}
public static void main(String[] args) {
System.out.println(solve());
}
}