Problem 914: Triangles inside Circles

View on Project Euler

Project Euler Problem 914 Solution

EulerSolve provides an optimized solution for Project Euler Problem 914, Triangles inside Circles, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary For a positive radius \(R\), let \(F(R)\) denote the largest possible inradius of a primitive integer right triangle that fits strictly inside a circle of radius \(R\). If the legs are \(a\) and \(b\), the hypotenuse is \(c\), and the inradius is \(r\), the challenge is to evaluate \(F(10^{18})\). The geometric condition is much simpler than it first appears. A right triangle has circumradius \(c/2\), so fitting strictly inside the circle is equivalent to requiring \(c<2R\). The solution therefore becomes an optimization over primitive Pythagorean triples, and the implementations succeed by combining an exact formula for \(r\), a monotonicity argument for fixed parameters, and an exact continuous upper bound that collapses the search interval. Mathematical Approach From the circle constraint to Euclid's parameters Every primitive Pythagorean triple can be written uniquely as $$a=m^2-n^2,\qquad b=2mn,\qquad c=m^2+n^2,$$ with integers \(m>n>0\), \(\gcd(m,n)=1\), and \(m\not\equiv n\pmod 2\)....

Detailed mathematical approach

Problem Summary

For a positive radius \(R\), let \(F(R)\) denote the largest possible inradius of a primitive integer right triangle that fits strictly inside a circle of radius \(R\). If the legs are \(a\) and \(b\), the hypotenuse is \(c\), and the inradius is \(r\), the challenge is to evaluate \(F(10^{18})\).

The geometric condition is much simpler than it first appears. A right triangle has circumradius \(c/2\), so fitting strictly inside the circle is equivalent to requiring \(c<2R\). The solution therefore becomes an optimization over primitive Pythagorean triples, and the implementations succeed by combining an exact formula for \(r\), a monotonicity argument for fixed parameters, and an exact continuous upper bound that collapses the search interval.

Mathematical Approach

From the circle constraint to Euclid's parameters

Every primitive Pythagorean triple can be written uniquely as

$$a=m^2-n^2,\qquad b=2mn,\qquad c=m^2+n^2,$$

with integers \(m>n>0\), \(\gcd(m,n)=1\), and \(m\not\equiv n\pmod 2\). Since the triangle is right-angled, its smallest enclosing circle has radius \(c/2\), so the condition \(c<2R\) becomes

$$m^2+n^2<2R.$$

Because the inequality is strict, it is convenient to set

$$T=2R-1,$$

so that admissible pairs are exactly those with

$$m^2+n^2\le T.$$

For a right triangle the inradius is

$$r=\frac{a+b-c}{2},$$

and substituting Euclid's formulas gives the remarkably simple objective

$$r=\frac{(m^2-n^2)+2mn-(m^2+n^2)}{2}=n(m-n).$$

So the entire problem becomes

$$F(R)=\max\left\{n(m-n):m>n>0,\ \gcd(m,n)=1,\ m\not\equiv n\pmod 2,\ m^2+n^2\le T\right\}.$$

Why each \(n\) has a single best partner

Fix \(n\). Then the score \(n(m-n)\) increases strictly with \(m\), so only the largest admissible \(m\) can be optimal for that \(n\). Ignoring parity and coprimality for a moment, the geometric ceiling is

$$m_{\max}(n)=\left\lfloor \sqrt{T-n^2}\right\rfloor.$$

If \(m_{\max}(n)\le n\), then no triangle exists for that \(n\). Otherwise the primitive-triple conditions are enforced in the only direction that matters: start from \(m_{\max}(n)\), decrease once if \(m\) has the same parity as \(n\), and then keep decreasing by \(2\) until \(\gcd(m,n)=1\). The first pair that survives is already the best one for that \(n\), because every later candidate has a smaller \(m\) and therefore a smaller inradius.

This also yields the natural outer range for the search. Since every feasible pair has \(m>n\), we must have

$$2n^2<m^2+n^2\le T,$$

hence

$$1\le n\le \left\lfloor \sqrt{\frac{T}{2}}\right\rfloor=\left\lfloor \sqrt{\frac{2R-1}{2}}\right\rfloor.$$

The continuous envelope that makes pruning exact

Now relax the arithmetic conditions and allow \(m\) to move on the real boundary \(m=\sqrt{T-n^2}\). Then every integer candidate with the same \(n\) satisfies

$$r\le U(n):=n\left(\sqrt{T-n^2}-n\right).$$

This function is a true upper bound, not a heuristic estimate. Differentiating \(U(n)\) shows that its unique peak occurs when the boundary value of \(m\) and the chosen \(n\) satisfy

$$m=(1+\sqrt 2)\,n.$$

Equivalently, the maximizing real \(n\) satisfies

$$n_0^2=\frac{T}{4+2\sqrt 2}=\frac{2R-1}{4+2\sqrt 2}.$$

So the best triangle must lie near one continuous optimum rather than somewhere arbitrary in the whole interval. Once a current record is known, every \(n\) with \(U(n)\) no larger than that record can be discarded immediately. Because \(U(n)\) rises to a single peak and then falls, the surviving region is one interval around \(n_0\), which can be found accurately by bisection on the left and right sides.

Worked Example: \(R=100\)

Here \(T=199\), so admissibility means

$$m^2+n^2\le 199.$$

The continuous peak is near

$$n_0\approx \sqrt{\frac{199}{4+2\sqrt 2}}\approx 5.40,$$

which already tells us that the optimum should occur close to \(n=5\). For \(n=4\),

$$m_{\max}(4)=\left\lfloor \sqrt{199-16}\right\rfloor=13.$$

The pair \((13,4)\) already has opposite parity and \(\gcd(13,4)=1\), so it produces

$$a=13^2-4^2=153,\qquad b=2\cdot 13\cdot 4=104,\qquad c=13^2+4^2=185,$$

and therefore

$$r=4(13-4)=36.$$

Since \(c/2=92.5<100\), the triangle really does fit inside the circle. Nearby candidates are slightly worse: \(n=5\) leads to \(m=12\) and \(r=35\), while \(n=6\) leads to \(m=11\) and \(r=30\). Thus \(F(100)=36\).

How the Code Works

Shared search strategy

The C++, Python, and Java implementations all follow the same logic. They compute exact integer square roots so that the strict boundary \(m^2+n^2<2R\) is handled as \(m^2+n^2\le 2R-1\) without any floating-point off-by-one errors. They then estimate the location of the continuous peak with \(n_0\approx\sqrt{(2R-1)/(4+2\sqrt 2)}\), scan a wide window around that point to obtain a strong initial record, and evaluate each tested \(n\) by starting from \(\lfloor\sqrt{2R-1-n^2}\rfloor\), fixing parity if necessary, and descending by \(2\) until a coprime partner is found.

Exact pruning and final sweep

After the initial window, the implementations use the upper envelope \(U(n)=n(\sqrt{2R-1-n^2}-n)\) to determine which \(n\)-values can still beat the current best answer. Because \(U\) is unimodal, bisection on each side of the peak isolates the real interval where improvement is still possible. That interval is then converted back to integers and widened slightly to stay safe against rounding.

Inside the final sweep, the code applies the sharper integer bound

$$n\left(\left\lfloor\sqrt{2R-1-n^2}\right\rfloor-n\right)$$

before spending time on gcd tests. If even this value cannot improve the current record, that \(n\) is skipped immediately. The pruning is therefore exact: every discarded case has been eliminated by a proved upper bound.

Complexity Analysis

The natural search variable is \(n\), and its full admissible range has size \(O(\sqrt R)\). For each tested \(n\), the implementation performs one integer square root, possibly one parity correction, and then a downward walk through values of the correct parity until it reaches a coprime partner. So the basic structure is an \(O(\sqrt R)\)-sized search with very small per-candidate work in practice.

The continuous bound \(U(n)\) makes the real running time much smaller than a full scan, because once a good record is found only a narrow band around the peak can remain relevant. Memory usage is \(O(1)\), since the algorithm stores only a small number of integer and floating-point variables.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=914
  2. Pythagorean triple: Wikipedia - Pythagorean triple
  3. Incircle and excircles of a triangle: Wikipedia - Incircle and excircles of a triangle
  4. Thales's theorem: Wikipedia - Thales's theorem
  5. Greatest common divisor: Wikipedia - Greatest common divisor
  6. Integer square root: Wikipedia - Integer square root

Problem 914 source code

C++

#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <numeric>

namespace {

using u64 = std::uint64_t;
using u128 = __uint128_t;

u64 isqrt_u128(u128 x) {
    long double y = std::sqrt(static_cast<long double>(x));
    u64 r = static_cast<u64>(y);
    while ((u128)(r + 1) * (r + 1) <= x) {
        ++r;
    }
    while ((u128)r * r > x) {
        --r;
    }
    return r;
}

u64 best_for_n(u64 R, u64 n) {
    const u128 C = (u128)2 * R;
    const u128 nn = (u128)n * n;
    if (nn >= C) {
        return 0;
    }

    const u128 t = C - 1 - nn;
    u64 m = isqrt_u128(t);
    if (m <= n) {
        return 0;
    }

    if (((m ^ n) & 1ULL) == 0ULL) {
        --m;
    }

    while (m > n && std::gcd(m, n) != 1) {
        m -= 2;
    }

    if (m <= n) {
        return 0;
    }

    return n * (m - n);
}

u64 exhaustive_F(u64 R) {
    const u128 C = (u128)2 * R;
    const u64 n_max = isqrt_u128((C - 1) / 2);

    u64 best = 0;
    for (u64 n = 1; n <= n_max; ++n) {
        const u64 cand = best_for_n(R, n);
        if (cand > best) {
            best = cand;
        }
    }
    return best;
}

u64 fast_F(u64 R) {
    const u128 C = (u128)2 * R;
    const long double sqrt2 = std::sqrt(2.0L);
    const long double denom = 4.0L + 2.0L * sqrt2;
    const long double Cld = static_cast<long double>(C - 1);

    u64 n0 = static_cast<u64>(std::llround(std::sqrt(Cld / denom)));
    if (n0 == 0) {
        n0 = 1;
    }

    u64 best = 0;
    constexpr int kWindow = 4096;
    for (int d = -kWindow; d <= kWindow; ++d) {
        const std::int64_t ni = static_cast<std::int64_t>(n0) + d;
        if (ni <= 0) {
            continue;
        }
        const u64 cand = best_for_n(R, static_cast<u64>(ni));
        if (cand > best) {
            best = cand;
        }
    }

    const auto ub_real = [&](long double n) {
        if (n <= 0) {
            return 0.0L;
        }
        const long double t = Cld - n * n;
        if (t <= 0) {
            return 0.0L;
        }
        return n * (std::sqrt(t) - n);
    };

    const long double n0ld = static_cast<long double>(n0);
    const long double nmaxld = std::sqrt(Cld / 2.0L);

    long double left = n0ld;
    long double right = n0ld;

    if (ub_real(n0ld) > static_cast<long double>(best)) {
        long double lo = 0.0L;
        long double hi = n0ld;
        for (int it = 0; it < 140; ++it) {
            const long double mid = (lo + hi) * 0.5L;
            if (ub_real(mid) > static_cast<long double>(best)) {
                hi = mid;
            } else {
                lo = mid;
            }
        }
        left = hi;

        lo = n0ld;
        hi = nmaxld;
        for (int it = 0; it < 140; ++it) {
            const long double mid = (lo + hi) * 0.5L;
            if (ub_real(mid) > static_cast<long double>(best)) {
                lo = mid;
            } else {
                hi = mid;
            }
        }
        right = lo;
    }

    u64 L = 1;
    if (left > 16.0L) {
        L = static_cast<u64>(std::floor(left)) - 16;
    }

    const u64 n_max = isqrt_u128((C - 1) / 2);
    u64 Rn = static_cast<u64>(std::ceil(right)) + 16;
    if (Rn > n_max) {
        Rn = n_max;
    }

    for (u64 n = L; n <= Rn; ++n) {
        const u128 nn = (u128)n * n;
        if (nn >= C) {
            break;
        }
        const u64 mmax = isqrt_u128(C - 1 - nn);
        if (mmax <= n) {
            continue;
        }
        const u64 ub = n * (mmax - n);
        if (ub <= best) {
            continue;
        }

        const u64 cand = best_for_n(R, n);
        if (cand > best) {
            best = cand;
        }
    }

    return best;
}

void validate() {
    assert(exhaustive_F(100) == 36);
    assert(fast_F(100) == 36);

    for (u64 R = 2; R <= 5000; ++R) {
        assert(fast_F(R) == exhaustive_F(R));
    }

    u64 seed = 1;
    for (int i = 0; i < 200; ++i) {
        seed = seed * 6364136223846793005ULL + 1ULL;
        const u64 R = 2 + (seed % 100'000'000ULL);
        assert(fast_F(R) == exhaustive_F(R));
    }
}

}  // namespace

int main() {
    validate();
    std::cout << fast_F(1'000'000'000'000'000'000ULL) << '\n';
    return 0;
}

Python

import math

def isqrt(x):
    return math.isqrt(x)

def best_for_n(R, n):
    C = 2 * R
    nn = n * n
    if nn >= C:
        return 0
        
    t = C - 1 - nn
    m = isqrt(t)
    if m <= n:
        return 0
        
    if ((m ^ n) & 1) == 0:
        m -= 1
        
    while m > n and math.gcd(m, n) != 1:
        m -= 2
        
    if m <= n:
        return 0
        
    return n * (m - n)

def fast_F(R):
    C = 2 * R
    sqrt2 = math.sqrt(2.0)
    denom = 4.0 + 2.0 * sqrt2
    Cld = float(C - 1)
    
    n0 = round(math.sqrt(Cld / denom))
    if n0 == 0:
        n0 = 1
        
    best = 0
    kWindow = 4096
    for d in range(-kWindow, kWindow + 1):
        ni = n0 + d
        if ni <= 0:
            continue
        cand = best_for_n(R, ni)
        if cand > best:
            best = cand
            
    def ub_real(n):
        if n <= 0:
            return 0.0
        t = Cld - n * n
        if t <= 0:
            return 0.0
        return n * (math.sqrt(t) - n)
        
    n0ld = float(n0)
    nmaxld = math.sqrt(Cld / 2.0)
    
    left = n0ld
    right = n0ld
    
    if ub_real(n0ld) > float(best):
        lo = 0.0
        hi = n0ld
        for _ in range(140):
            mid = (lo + hi) * 0.5
            if ub_real(mid) > float(best):
                hi = mid
            else:
                lo = mid
        left = hi
        
        lo = n0ld
        hi = nmaxld
        for _ in range(140):
            mid = (lo + hi) * 0.5
            if ub_real(mid) > float(best):
                lo = mid
            else:
                hi = mid
        right = lo
        
    L = 1
    if left > 16.0:
        L = int(math.floor(left)) - 16
        
    n_max = isqrt((C - 1) // 2)
    Rn = int(math.ceil(right)) + 16
    if Rn > n_max:
        Rn = n_max
        
    for n in range(L, Rn + 1):
        nn = n * n
        if nn >= C:
            break
        mmax = isqrt(C - 1 - nn)
        if mmax <= n:
            continue
        ub = n * (mmax - n)
        if ub <= best:
            continue
            
        cand = best_for_n(R, n)
        if cand > best:
            best = cand
            
    return best

def solve():
    return str(fast_F(1000000000000000000))

if __name__ == "__main__":
    print(solve())

Java

public class Euler914 {

    static long isqrt(long x) {
        long r = (long) Math.sqrt((double) x);
        while ((r + 1) * (r + 1) <= x) {
            ++r;
        }
        while (r * r > x) {
            --r;
        }
        return r;
    }

    static long gcd(long a, long b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    static long bestForN(long R, long n) {
        long C = 2 * R;
        long nn = n * n;
        if (nn >= C) {
            return 0;
        }

        long t = C - 1 - nn;
        long m = isqrt(t);
        if (m <= n) {
            return 0;
        }

        if (((m ^ n) & 1L) == 0L) {
            --m;
        }

        while (m > n && gcd(m, n) != 1) {
            m -= 2;
        }

        if (m <= n) {
            return 0;
        }

        return n * (m - n);
    }

    static long fastF(long R) {
        long C = 2 * R;
        double sqrt2 = Math.sqrt(2.0);
        double denom = 4.0 + 2.0 * sqrt2;
        double Cld = (double) (C - 1);

        long n0 = Math.round(Math.sqrt(Cld / denom));
        if (n0 == 0) {
            n0 = 1;
        }

        long best = 0;
        int kWindow = 4096;
        for (int d = -kWindow; d <= kWindow; ++d) {
            long ni = n0 + d;
            if (ni <= 0) {
                continue;
            }
            long cand = bestForN(R, ni);
            if (cand > best) {
                best = cand;
            }
        }

        double n0ld = (double) n0;
        double nmaxld = Math.sqrt(Cld / 2.0);

        double left = n0ld;
        double right = n0ld;

        double ubN0 = n0ld <= 0 || Cld - n0ld * n0ld <= 0 ? 0.0 : n0ld * (Math.sqrt(Cld - n0ld * n0ld) - n0ld);

        if (ubN0 > (double) best) {
            double lo = 0.0;
            double hi = n0ld;
            for (int it = 0; it < 140; ++it) {
                double mid = (lo + hi) * 0.5;
                double ubMid = mid <= 0 || Cld - mid * mid <= 0 ? 0.0 : mid * (Math.sqrt(Cld - mid * mid) - mid);
                if (ubMid > (double) best) {
                    hi = mid;
                } else {
                    lo = mid;
                }
            }
            left = hi;

            lo = n0ld;
            hi = nmaxld;
            for (int it = 0; it < 140; ++it) {
                double mid = (lo + hi) * 0.5;
                double ubMid = mid <= 0 || Cld - mid * mid <= 0 ? 0.0 : mid * (Math.sqrt(Cld - mid * mid) - mid);
                if (ubMid > (double) best) {
                    lo = mid;
                } else {
                    hi = mid;
                }
            }
            right = lo;
        }

        long L = 1;
        if (left > 16.0) {
            L = (long) Math.floor(left) - 16;
        }

        long nMax = isqrt((C - 1) / 2);
        long Rn = (long) Math.ceil(right) + 16;
        if (Rn > nMax) {
            Rn = nMax;
        }

        for (long n = L; n <= Rn; ++n) {
            long nn = n * n;
            if (nn >= C) {
                break;
            }
            long mmax = isqrt(C - 1 - nn);
            if (mmax <= n) {
                continue;
            }
            long ub = n * (mmax - n);
            if (ub <= best) {
                continue;
            }

            long cand = bestForN(R, n);
            if (cand > best) {
                best = cand;
            }
        }

        return best;
    }

    public static String solve() {
        return Long.toString(fastF(1000000000000000000L));
    }

    public static void main(String[] args) {
        System.out.println(solve());
    }
}