Problem 476: Circle Packing II

View on Project Euler

Project Euler Problem 476 Solution

EulerSolve provides an optimized solution for Project Euler Problem 476, Circle Packing II, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Let $$T_n=\left\{(a,b,c)\in \mathbb{Z}_{>0}^3:1\le a\le b\le c,\ c\le a+b-1,\ a+b\le n\right\}.$$ For each integer triangle in \(T_n\), place the incircle first. Then, in every corner, continue packing circles that are tangent to the two sides of that angle and to the previous circle in the same corner. The quantity being averaged is the total area of the incircle together with the two largest additional circles that appear after this first placement. If we write that triangle contribution as \(F(a,b,c)\), then the implementations compute $$S(n)=\frac{1}{|T_n|}\sum_{(a,b,c)\in T_n} F(a,b,c).$$ The numerical checkpoints used by the implementations are $$S(2)=0.31998,\qquad S(5)=1.25899.$$ Mathematical Approach Fix one triangle with side lengths \(a\le b\le c\), and let \(A\le B\le C\) be the opposite angles. The geometry simplifies because the side ordering already tells us which corners can contain the largest packed circles. Step 1: Describe the admissible triangles and angle order The inequalities $$1\le a\le b\le c,\qquad c\le a+b-1,\qquad a+b\le n$$ say that we average over all nondegenerate integer triangles in sorted side order. Since larger sides face larger angles, the ordering of the angles is $$A\le B\le C.$$ This matters because a narrower corner produces a larger next tangent circle....

Detailed mathematical approach

Problem Summary

Let

$$T_n=\left\{(a,b,c)\in \mathbb{Z}_{>0}^3:1\le a\le b\le c,\ c\le a+b-1,\ a+b\le n\right\}.$$

For each integer triangle in \(T_n\), place the incircle first. Then, in every corner, continue packing circles that are tangent to the two sides of that angle and to the previous circle in the same corner. The quantity being averaged is the total area of the incircle together with the two largest additional circles that appear after this first placement.

If we write that triangle contribution as \(F(a,b,c)\), then the implementations compute

$$S(n)=\frac{1}{|T_n|}\sum_{(a,b,c)\in T_n} F(a,b,c).$$

The numerical checkpoints used by the implementations are

$$S(2)=0.31998,\qquad S(5)=1.25899.$$

Mathematical Approach

Fix one triangle with side lengths \(a\le b\le c\), and let \(A\le B\le C\) be the opposite angles. The geometry simplifies because the side ordering already tells us which corners can contain the largest packed circles.

Step 1: Describe the admissible triangles and angle order

The inequalities

$$1\le a\le b\le c,\qquad c\le a+b-1,\qquad a+b\le n$$

say that we average over all nondegenerate integer triangles in sorted side order. Since larger sides face larger angles, the ordering of the angles is

$$A\le B\le C.$$

This matters because a narrower corner produces a larger next tangent circle. So once the sides are sorted, the smallest angle \(A\) is the first place to look for the biggest circle after the incircle.

Step 2: Compute the incircle radius from the side lengths

Set

$$p=a+b+c,\qquad s=\frac{p}{2},\qquad x=p-2a,\qquad y=p-2b,\qquad z=p-2c.$$

Then \(x=2(s-a)\), \(y=2(s-b)\), and \(z=2(s-c)\). If \(\Delta\) is the triangle area, Heron's formula gives

$$\Delta^2=s(s-a)(s-b)(s-c).$$

The inradius is \(r=\Delta/s\), so

$$r^2=\frac{\Delta^2}{s^2}=\frac{(s-a)(s-b)(s-c)}{s}=\frac{xyz}{4p}.$$

This is exactly the compact formula used by the implementations. It avoids computing the area explicitly before the final multiplication by \(\pi\).

Step 3: Derive the geometric ratio inside one corner

Consider one angle \(\theta\). Any circle tangent to both sides of that angle has its center on the angle bisector. If the radius is \(\rho\), then the center is at distance

$$d=\frac{\rho}{\sin(\theta/2)}$$

from the vertex.

Now take two consecutive circles in the same corner, with radii \(R\) and \(R'\), both tangent to the sides and tangent to each other. Their centers lie on the same bisector, and external tangency gives

$$\frac{R-R'}{\sin(\theta/2)}=R+R'.$$

Solving for the ratio yields

$$\frac{R'}{R}=\frac{1-\sin(\theta/2)}{1+\sin(\theta/2)}=:q(\theta).$$

So each corner generates a geometric chain of radii

$$r,\ rq(\theta),\ rq(\theta)^2,\ rq(\theta)^3,\dots$$

starting from the incircle. For the two angles that actually matter in the final comparison, the half-angle identities are

$$\sin^2\left(\frac{A}{2}\right)=\frac{(s-b)(s-c)}{bc}=\frac{yz}{4bc},$$

$$\sin^2\left(\frac{B}{2}\right)=\frac{(s-a)(s-c)}{ac}=\frac{xz}{4ac}.$$

Step 4: Identify the two largest extra circles

The function \(q(\theta)\) decreases as \(\theta\) increases on \((0,\pi)\). Because \(A\le B\le C\), we have

$$q_A\ge q_B\ge q_C,$$

where \(q_A=q(A)\), \(q_B=q(B)\), and \(q_C=q(C)\).

Therefore the largest circle after the incircle is always the first corner-circle in angle \(A\), with radius \(rq_A\).

For the third selected circle, only two candidates can survive:

$$rq_B,\qquad rq_A^2.$$

The first circle in angle \(C\) is no larger than the first circle in angle \(B\), and every later circle in any corner is no larger than the first unused circle in that same corner. So the third radius is

$$r\max\{q_B,q_A^2\}.$$

Squaring the radii to convert them into area factors, the two extra contributions are

$$q_A^2,\qquad \max\{q_B^2,q_A^4\}.$$

Step 5: Final contribution of one triangle

The incircle area is \(\pi r^2\). Adding the two largest extra circles gives

$$F(a,b,c)=\pi r^2\left(1+q_A^2+\max\{q_B^2,q_A^4\}\right).$$

This is the exact formula accumulated by the implementations for every admissible triangle.

Worked Example: The equilateral triangle \((1,1,1)\)

For \(a=b=c=1\), we have

$$p=3,\qquad s=\frac{3}{2},\qquad x=y=z=1.$$

Hence

$$r^2=\frac{xyz}{4p}=\frac{1}{12}.$$

All angles equal \(\pi/3\), so

$$\sin\left(\frac{A}{2}\right)=\sin\left(\frac{\pi}{6}\right)=\frac{1}{2},\qquad q_A=q_B=\frac{1-\frac12}{1+\frac12}=\frac13.$$

Therefore

$$F(1,1,1)=\pi\cdot\frac{1}{12}\left(1+\frac{1}{9}+\max\left\{\frac{1}{9},\frac{1}{81}\right\}\right)=\pi\cdot\frac{1}{12}\left(1+\frac{1}{9}+\frac{1}{9}\right)=\frac{11\pi}{108}.$$

Numerically,

$$\frac{11\pi}{108}\approx 0.31998.$$

Since \(T_2\) contains only this one triangle, we get \(S(2)=0.31998\), matching the checkpoint.

How the Code Works

The C++, Python, and Java implementations all evaluate the same formula. They enumerate the admissible triangles in sorted order by looping over \(a\), then \(b\), then every feasible \(c\) with

$$b\le c\le a+b-1.$$

For each triangle, the implementation computes \(p\), \(x\), \(y\), and \(z\), obtains \(r^2=xyz/(4p)\), then evaluates the half-angle terms needed for \(A\) and \(B\). From those values it builds \(q_A\) and \(q_B\), forms

$$\pi r^2\left(1+q_A^2+\max\{q_B^2,q_A^4\}\right),$$

adds that quantity to a running total, and increments the triangle counter.

The final answer is the total divided by the number of triangles. The Java implementation uses compensated accumulation to reduce floating-point loss, while the C++ computation uses extended precision. The Python implementation delegates to the same compiled numerical routine, so all three implementations share the same geometry and the same checkpoints.

Complexity Analysis

The number of admissible triangles is

$$|T_n|=\sum_{a=1}^{\lfloor n/2\rfloor}\sum_{b=a}^{n-a} a,$$

because for fixed \(a\) and \(b\), the value of \(c\) runs from \(b\) to \(a+b-1\), which gives exactly \(a\) possibilities. This count is \(\Theta(n^3)\), so the overall runtime is \(\Theta(n^3)\).

The work done for each triangle is constant-time arithmetic with a few square roots and one maximum comparison. Memory usage is \(O(1)\), since the implementations only maintain a running total, a counter, and a small number of temporary floating-point values.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=476
  2. Incircle and excentral geometry: Wikipedia — Incircle and excircles of a triangle
  3. Heron's formula: Wikipedia — Heron's formula
  4. Half-angle identities: Wikipedia — Half-angle formulae

Problem 476 source code

C++

#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string>

namespace {

struct Options {
    int n = 1803;
    bool run_checkpoints = true;
};

bool parse_int_after_prefix(const std::string& arg, const std::string& prefix, int& out) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    try {
        out = std::stoi(tail);
    } catch (...) {
        return false;
    }
    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 (parse_int_after_prefix(arg, "--n=", options.n)) {
            continue;
        }
        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    if (options.n < 2) {
        std::cerr << "--n must be at least 2.\n";
        return false;
    }
    return true;
}

long double solve_average(const int n) {
    constexpr long double kPi = 3.141592653589793238462643383279502884L;
    long double total = 0.0L;
    std::uint64_t count = 0;

    for (int a = 1; a <= n; ++a) {
        for (int b = a; a + b <= n; ++b) {
            const int c_max = a + b - 1;
            for (int c = b; c <= c_max; ++c) {
                const long double p = static_cast<long double>(a + b + c);
                const long double x = p - 2.0L * static_cast<long double>(a);  // b+c-a
                const long double y = p - 2.0L * static_cast<long double>(b);  // a+c-b
                const long double z = p - 2.0L * static_cast<long double>(c);  // a+b-c

                // Inradius squared: r^2 = ((b+c-a)(a+c-b)(a+b-c)) / (4(a+b+c))
                const long double r2 = (x * y * z) / (4.0L * p);

                // For angle A (opposite side a): sin(A/2)^2 = ((s-b)(s-c))/(bc)
                const long double sin_half_a =
                    std::sqrt((y * z) / (4.0L * static_cast<long double>(b) * c));
                // For angle B (opposite side b): sin(B/2)^2 = ((s-a)(s-c))/(ac)
                const long double sin_half_b =
                    std::sqrt((x * z) / (4.0L * static_cast<long double>(a) * c));

                const long double q_a = (1.0L - sin_half_a) / (1.0L + sin_half_a);
                const long double q_b = (1.0L - sin_half_b) / (1.0L + sin_half_b);

                // After placing the incircle, each corner yields a geometric chain of circles.
                // The largest extra circle is always in corner A (smallest angle), and the third
                // circle is either in corner B or the next one in corner A.
                const long double extra2 = q_a * q_a;
                const long double extra3 = std::max(q_b * q_b, extra2 * extra2);
                const long double area = kPi * r2 * (1.0L + extra2 + extra3);

                total += area;
                ++count;
            }
        }
    }

    return total / static_cast<long double>(count);
}

bool nearly_equal(const long double a, const long double b, const long double eps) {
    return std::fabsl(a - b) <= eps;
}

bool run_checkpoints() {
    if (!nearly_equal(solve_average(2), 0.31998L, 5e-5L)) {
        std::cerr << "Checkpoint failed: S(2)\n";
        return false;
    }
    if (!nearly_equal(solve_average(5), 1.25899L, 5e-5L)) {
        std::cerr << "Checkpoint failed: S(5)\n";
        return false;
    }
    return true;
}

}  // namespace

int main(int argc, char** argv) {
    Options options;
    if (!parse_arguments(argc, argv, options)) {
        return 1;
    }

    if (options.run_checkpoints && !run_checkpoints()) {
        return 1;
    }

    const long double ans = solve_average(options.n);
    std::cout << std::fixed << std::setprecision(5) << static_cast<double>(ans) << '\n';
    return 0;
}

Python

from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path

ANSWER_RE = re.compile(r"answer\s*:\s*(.+)$", re.IGNORECASE)
EQUAL_RE = re.compile(r"=\s*(.+)$")


def parse_output(stdout: str) -> str:
    lines = [line.strip() for line in stdout.splitlines() if line.strip()]
    if not lines:
        return ""
    answers = []
    equals = []
    for line in lines:
        m1 = ANSWER_RE.search(line)
        if m1:
            answers.append(m1.group(1).strip())
        m2 = EQUAL_RE.search(line)
        if m2:
            equals.append(m2.group(1).strip())
    if answers:
        return answers[-1]
    if equals:
        return equals[-1]
    return lines[-1]


def should_skip_cpp_checkpoints(src: Path) -> bool:
    try:
        text = src.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return False
    return "--skip-checkpoints" in text


def run_cpp(binary: Path, src: Path, root: Path) -> str:
    cmd = [str(binary)]
    if should_skip_cpp_checkpoints(src):
        cmd.append("--skip-checkpoints")

    try:
        return subprocess.check_output(cmd, text=True, cwd=root)
    except subprocess.CalledProcessError:
        return subprocess.check_output(cmd, text=True, cwd=src.parent)


def solve() -> str:
    problem_id = __file__.split("Euler")[-1].split(".")[0]
    root = Path(__file__).resolve().parent.parent
    src = root / "solutionsCpp" / f"Euler{problem_id}.cpp"
    binary = root / "solutionsCpp" / f".euler{problem_id}_py_bridge"

    if not binary.exists() or src.stat().st_mtime > binary.stat().st_mtime:
        compiler = shutil.which("clang++") or shutil.which("g++")
        if not compiler:
            raise RuntimeError("No C++ compiler found (clang++/g++).")
        subprocess.check_call([compiler, "-std=c++17", "-O2", str(src), "-o", str(binary)])

    output = run_cpp(binary=binary, src=src, root=root)
    parsed = parse_output(output)
    if not parsed:
        raise RuntimeError(f"Euler{problem_id} bridge produced empty output.")
    return parsed


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

Java

public class Euler476 {

    private static double solveAverage(int n) {
        double kPi = 3.141592653589793238462643383279502884;
        double total = 0.0;
        double cComp = 0.0;
        long count = 0;

        for (int a = 1; a <= n; ++a) {
            for (int b = a; b <= n - a; ++b) {
                int cMax = a + b - 1;
                for (int c = b; c <= cMax; ++c) {
                    double p = a + b + c;
                    double x = p - 2.0 * a;
                    double y = p - 2.0 * b;
                    double z = p - 2.0 * c;

                    double r2 = (x * y * z) / (4.0 * p);

                    double sinHalfA = Math.sqrt((y * z) / (4.0 * b * c));
                    double sinHalfB = Math.sqrt((x * z) / (4.0 * a * c));

                    double qA = (1.0 - sinHalfA) / (1.0 + sinHalfA);
                    double qB = (1.0 - sinHalfB) / (1.0 + sinHalfB);

                    double extra2 = qA * qA;
                    double extra3 = Math.max(qB * qB, extra2 * extra2);
                    double area = kPi * r2 * (1.0 + extra2 + extra3);

                    double yComp = area - cComp;
                    double tComp = total + yComp;
                    cComp = (tComp - total) - yComp;
                    total = tComp;

                    count++;
                }
            }
        }

        return total / count;
    }

    public static void main(String[] args) {
        int n = 1803;
        double ans = solveAverage(n);
        System.out.printf(java.util.Locale.US, "%.5f\n", ans);
    }
}