Problem Summary

For each integer circumradius \(r\), let \(T(r)\) be the number of triangles with integer side lengths and circumradius exactly \(r\). The program computes

$$S(N)=\sum_{r=1}^{N} r\,T(r),\qquad N=10^7.$$

A brute-force search over all triples \((a,b,c)\) up to \(2r\) would be far too slow, so the implementation first characterizes which side lengths can occur for a fixed radius and then checks only those candidates exactly.

Mathematical Approach

Step 1: Convert the circumradius condition into an exact integer identity

For a triangle with sides \(a,b,c\) and area \(\Delta\), Heron's formula gives

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

The circumradius also satisfies

$$R=\frac{abc}{4\Delta}.$$

Squaring and eliminating \(\Delta\) yields

$$a^2b^2c^2=R^2(a+b+c)(a+b-c)(a-b+c)(-a+b+c).$$

This is exactly the predicate used in the solver. It avoids floating-point roundoff entirely: once \(a\), \(b\), \(c\), and \(r\) are integers, the question “is the circumradius equal to \(r\)?” becomes a pure integer comparison.

Step 2: Describe every possible side length for a fixed radius

If side \(a\) is opposite angle \(A\), then the extended law of sines gives

$$a=2r\sin A.$$

Because \(a\) and \(r\) are integers, \(\sin A=a/(2r)\) is rational. Rational points on the unit circle are parameterized by primitive Pythagorean data: for coprime integers \(m>n\ge 1\) with opposite parity,

$$\left(\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right)$$

is a primitive rational point on \(x^2+y^2=1\). Therefore every rational sine in \((0,1)\) can be written as one of

$$\sin A\in\left\{\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right\}.$$

If we write \(d=m^2+n^2\) and \(u\) for one of the two numerators, then

$$a=2r\frac{u}{d}.$$

So \(a\) is guaranteed to be integral whenever \(d\mid r\). The special value \(\sin A=1\) is not produced by finite \((m,n)\), so the code inserts the diameter case \(a=2r\) explicitly.

Step 3: Precompute useful denominators once, then use only divisors of \(r\)

The C++ solver builds a table indexed by \(d\le N\). For each primitive pair \((m,n)\) with \(d=m^2+n^2\le N\), it stores the admissible numerators

$$u\in\{m^2-n^2,\ 2mn\}.$$

For a fixed radius \(r\), only denominators dividing \(r\) can contribute integer sides. If \(r=kd\), then each stored numerator produces the candidate side

$$a=2ku.$$

This is why the implementation first factors \(r\) using a smallest-prime-factor sieve, enumerates all divisors of \(r\), and looks up only those divisor entries. The resulting candidate set is

$$\mathcal{S}_r=\{2r\}\cup \left\{2\frac{r}{d}u : d\mid r,\ u\in U(d)\right\},$$

where \(U(d)\) is the precomputed list of numerators attached to denominator \(d\). The list is sorted and deduplicated because distinct parameter pairs can lead to the same side length.

Step 4: Count valid triples from the candidate side set

After building \(\mathcal{S}_r\), the solver enumerates

$$a\le b\le c,\qquad a,b,c\in \mathcal{S}_r.$$

The sorted order gives an immediate pruning rule: as soon as \(a+b\le c\), the triangle inequality fails and the inner loop can stop for that pair \((a,b)\). For each remaining triple, the solver applies the exact identity from Step 1. Every triple that passes contributes \(1\) to \(T(r)\).

A simple example is \(r=5\). The primitive pair \((m,n)=(2,1)\) gives \(d=5\) and numerators \(3\) and \(4\). Because \(5\mid r\), the candidate sides include

$$2\cdot \frac{5}{5}\cdot 3=6,\qquad 2\cdot \frac{5}{5}\cdot 4=8,\qquad 2r=10.$$

So \((6,8,10)\) appears naturally, and the exact check confirms that its circumradius is indeed \(5\).

How the Code Works

The class Euler373Solver precomputes two structures: a smallest-prime-factor array for fast divisor generation, and a denominator-to-numerators table for rational-sine candidates. The method count_triangles_for_radius(r) builds \(\mathcal{S}_r\), loops over ordered triples, and calls circumradius_equals(a,b,c,r) for the exact test.

The C++ implementation uses a custom 192-bit accumulator U192 because the products in the circumradius identity are too large for ordinary 64-bit arithmetic. The outer sum over radii is embarrassingly parallel, so the solver distributes radii across worker threads with an atomic counter and combines per-thread partial sums. The Python and Java files are thin bridges that compile and run this same C++ solver, so all three language solutions share identical mathematics and validation.

Before solving the full problem, the program checks the optimized method against brute force for \(r\le 60\) and also verifies the checkpoints

$$S(100)=4950,\qquad S(1200)=1653605.$$

Complexity Analysis

Let \(N\) be the radius limit. Building the smallest-prime-factor table costs \(O(N\log\log N)\) time and \(O(N)\) memory. The denominator table iterates over primitive pairs with \(m^2+n^2\le N\), which is a one-time precomputation.

For one radius \(r\), divisor enumeration is proportional to the number of divisors of \(r\), and the dominant work is the triple scan over the deduplicated candidate set \(\mathcal{S}_r\). In the worst case this stage is \(O(|\mathcal{S}_r|^3)\), but in practice it is kept manageable because only divisors of \(r\) contribute candidates, duplicates are removed, and the triangle inequality stops many inner-loop iterations early. The final summation over radii is parallelized.

References

  1. Problem page: https://projecteuler.net/problem=373
  2. Circumradius formulas and the extended law of sines: Wikipedia — Circumscribed circle
  3. Heron's formula: Wikipedia — Heron's formula
  4. Primitive Pythagorean triples and rational points on the unit circle: Wikipedia — Pythagorean triple

Problemzusammenfassung

Für jeden ganzzahligen Umkreisradius \(r\) sei \(T(r)\) die Anzahl der Dreiecke mit ganzzahligen Seitenlängen und genau diesem Umkreisradius. Gesucht ist

$$S(N)=\sum_{r=1}^{N} r\,T(r),\qquad N=10^7.$$

Ein naiver Durchlauf über alle Dreiecksseiten bis \(2r\) wäre viel zu teuer. Deshalb erzeugt das Programm zunächst nur solche Seitenlängen, die für den festen Radius \(r\) überhaupt möglich sind, und prüft danach nur diese Kandidaten exakt.

Mathematischer Ansatz

Schritt 1: Die Radiusbedingung als exakte Ganzzahlgleichung formulieren

Für ein Dreieck mit Seiten \(a,b,c\) und Fläche \(\Delta\) gilt nach Heron

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

Außerdem ist der Umkreisradius

$$R=\frac{abc}{4\Delta}.$$

Nach Quadrieren und Eliminieren von \(\Delta\) erhält man

$$a^2b^2c^2=R^2(a+b+c)(a+b-c)(a-b+c)(-a+b+c).$$

Genau diese Identität verwendet der Solver. Statt Gleitkommazahlen zu vergleichen, arbeitet der Test nur mit Ganzzahlen und entscheidet exakt, ob der Umkreisradius gleich \(r\) ist.

Schritt 2: Alle möglichen Seiten aus rationalen Sinuswerten gewinnen

Ist \(a\) die Seite gegenüber dem Winkel \(A\), dann liefert das erweiterte Sinusgesetz

$$a=2r\sin A.$$

Da \(a\) und \(r\) ganzzahlig sind, ist \(\sin A=a/(2r)\) rational. Rationale Punkte auf dem Einheitskreis lassen sich durch primitive pythagoreische Parameter beschreiben: Für teilerfremde ganze Zahlen \(m>n\ge 1\) mit verschiedener Parität gilt

$$\left(\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right)\in x^2+y^2=1.$$

Daher hat jeder rationale Sinus in \((0,1)\) eine Darstellung der Form

$$\sin A\in\left\{\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right\}.$$

Schreibt man \(d=m^2+n^2\) und \(u\) für einen der beiden Zähler, so folgt

$$a=2r\frac{u}{d}.$$

Die Seitenlänge ist also sicher ganzzahlig, sobald \(d\mid r\) gilt. Der Spezialfall \(\sin A=1\) wird durch kein endliches \((m,n)\) erzeugt; deshalb fügt der Code den Durchmesser \(a=2r\) ausdrücklich hinzu.

Schritt 3: Einmalige Vorberechnung der Nenner, dann nur Teiler von \(r\) benutzen

Die C++-Implementierung baut eine Tabelle auf, die jedem Nenner \(d\le N\) die zugehörigen zulässigen Zähler zuordnet. Für jedes primitive Paar \((m,n)\) mit \(d=m^2+n^2\le N\) werden

$$u\in\{m^2-n^2,\ 2mn\}$$

eingetragen. Für einen festen Radius \(r\) können nur solche Nenner beitragen, die \(r\) teilen. Ist \(r=kd\), dann entsteht aus jedem gespeicherten Zähler die Kandidatenseite

$$a=2ku.$$

Darum faktorisiert der Solver \(r\) zunächst mit einer SPF-Tabelle, erzeugt alle Teiler von \(r\) und schlägt nur diese Nenner in der Tabelle nach. Die Kandidatenmenge lautet

$$\mathcal{S}_r=\{2r\}\cup \left\{2\frac{r}{d}u : d\mid r,\ u\in U(d)\right\}.$$

Danach wird sortiert und dedupliziert, weil verschiedene Parameterisierungen dieselbe Seitenlänge erzeugen können.

Schritt 4: Dreiecke aus der Kandidatenmenge zählen

Nun werden alle geordneten Tripel

$$a\le b\le c,\qquad a,b,c\in \mathcal{S}_r$$

durchlaufen. Wegen der Sortierung kann die innerste Schleife sofort abbrechen, sobald \(a+b\le c\) ist, denn dann ist die Dreiecksungleichung verletzt. Für jedes verbleibende Tripel prüft das Programm die exakte Gleichung aus Schritt 1. Jeder Treffer erhöht \(T(r)\) um \(1\).

Ein anschauliches Beispiel ist \(r=5\). Das primitive Paar \((2,1)\) liefert \(d=5\) und die Zähler \(3\) und \(4\). Weil \(5\mid r\), entstehen die Seiten

$$2\cdot \frac{5}{5}\cdot 3=6,\qquad 2\cdot \frac{5}{5}\cdot 4=8,\qquad 2r=10.$$

Damit erscheint das Tripel \((6,8,10)\), und der exakte Test bestätigt den Umkreisradius \(5\).

Wie der Code arbeitet

Die Klasse Euler373Solver besitzt zwei zentrale Vorberechnungen: eine SPF-Struktur für schnelle Teilererzeugung und eine Tabelle denominator_to_numerators für die rationalen Sinuswerte. Die Methode count_triangles_for_radius(r) baut daraus \(\mathcal{S}_r\), enumeriert die Seiten-Tripel und ruft circumradius_equals(a,b,c,r) für den exakten Nachweis auf.

Weil die Produkte in der Identität sehr groß werden, benutzt die C++-Lösung einen eigenen 192-Bit-Akkumulator U192. Die äußere Summe über alle Radien ist unabhängig und wird daher über mehrere Threads mit atomarem Radius-Zähler parallelisiert. Die Python- und Java-Dateien sind nur Brücken, die genau diesen C++-Solver kompilieren und ausführen; mathematisch handelt es sich also um dieselbe Lösung.

Vor der endgültigen Rechnung verifiziert das Programm die schnelle Methode gegen Brute Force für \(r\le 60\) und prüft zusätzlich

$$S(100)=4950,\qquad S(1200)=1653605.$$

Komplexitätsanalyse

Mit \(N\) als Radiusgrenze kostet das SPF-Sieb \(O(N\log\log N)\) Zeit und \(O(N)\) Speicher. Die Nenner-Tabelle wird einmalig über primitive Paare mit \(m^2+n^2\le N\) aufgebaut.

Für einen einzelnen Radius \(r\) ist die Teilererzeugung proportional zur Anzahl seiner Teiler; dominant ist dann die Suche über die deduplizierte Kandidatenmenge \(\mathcal{S}_r\). Im Worst Case ist dieser Schritt \(O(|\mathcal{S}_r|^3)\), praktisch wird er aber durch die starke arithmetische Vorfilterung, das Entfernen von Duplikaten und das frühe Abbrechen bei \(a+b\le c\) deutlich entschärft. Die Summe über alle \(r\) wird parallel ausgeführt.

Quellen

  1. Problemseite: https://projecteuler.net/problem=373
  2. Umkreis und erweitertes Sinusgesetz: Wikipedia — Umkreis
  3. Heronsche Formel: Wikipedia — Heronsche Formel
  4. Pythagoreische Tripel: Wikipedia — Pythagoreisches Tripel

Problem Özeti

Her tam sayı çevrel yarıçap \(r\) için, \(T(r)\) değeri çevrel yarıçapı tam olarak \(r\) olan tam kenarlı üçgenlerin sayısı olsun. Programın hesapladığı büyüklük

$$S(N)=\sum_{r=1}^{N} r\,T(r),\qquad N=10^7.$$

Her \(r\) için tüm \((a,b,c)\) üçlülerini kaba kuvvetle denemek mümkün değildir. Bu yüzden çözüm önce o yarıçapta hangi kenar uzunluklarının ortaya çıkabileceğini üretir, sonra sadece bu aday üçlüleri kesin olarak doğrular.

Matematiksel Yaklaşım

Adım 1: Çevrel yarıçap koşulunu tam sayı özdeşliğine dönüştürmek

Kenarları \(a,b,c\), alanı \(\Delta\) olan bir üçgen için Heron formülü

$$16\Delta^2=(a+b+c)(a+b-c)(a-b+c)(-a+b+c)$$

şeklindedir. Ayrıca çevrel yarıçap

$$R=\frac{abc}{4\Delta}$$

olur. \(\Delta\) elimine edilirse

$$a^2b^2c^2=R^2(a+b+c)(a+b-c)(a-b+c)(-a+b+c)$$

eşitliği elde edilir. Kod tam olarak bu eşitliği test eder. Böylece kayan nokta hataları tamamen ortadan kalkar; \(a\), \(b\), \(c\) ve \(r\) verildiğinde kontrol yalnızca tam sayı çarpımlarıyla yapılır.

Adım 2: Sabit bir yarıçap için mümkün olan kenarları üretmek

\(a\) kenarı açı \(A\)'nın karşısında ise, genişletilmiş sinüs teoremi

$$a=2r\sin A$$

der. \(a\) ve \(r\) tam sayı olduğundan \(\sin A=a/(2r)\) rasyoneldir. Birim çember üzerindeki rasyonel noktalar primitif Pisagor parametreleriyle yazılır: aralarında asal, \(m>n\ge 1\) ve biri tek biri çift olan sayılar için

$$\left(\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right)$$

birim çember üzerindedir. Dolayısıyla \((0,1)\) aralığındaki her rasyonel sinüs değeri

$$\sin A\in\left\{\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right\}$$

şeklinde yazılabilir. \(d=m^2+n^2\) ve \(u\) bu iki paydan biri olsun. O zaman

$$a=2r\frac{u}{d}$$

olur. Demek ki \(d\mid r\) ise \(a\) kesinlikle tam sayıdır. \(\sin A=1\) değeri sonlu bir \((m,n)\) çiftinden gelmediği için kod ayrıca çap durumunu, yani \(a=2r\) adayını, doğrudan ekler.

Adım 3: Paydaları bir kez önceden üretmek, sonra yalnızca \(r\)'nin bölenlerini kullanmak

C++ çözümü, her \(d\le N\) için bir tablo kurar. \(d=m^2+n^2\le N\) olan her primitif \((m,n)\) çifti için

$$u\in\{m^2-n^2,\ 2mn\}$$

değerleri bu tabloya yazılır. Sabit bir \(r\) için ancak \(r\)'yi bölen paydalar tam sayı kenar üretebilir. Eğer \(r=kd\) ise her kayıtlı pay

$$a=2ku$$

kenarını verir. Bu yüzden implementasyon önce en küçük asal bölen dizisiyle \(r\)'yi çarpanlarına ayırır, bütün bölenlerini çıkarır ve sadece bu bölenlere karşılık gelen kayıtları kullanır. Böylece aday kümesi

$$\mathcal{S}_r=\{2r\}\cup \left\{2\frac{r}{d}u : d\mid r,\ u\in U(d)\right\}$$

olur. Aynı kenar birden fazla parametreleştirmeden gelebileceği için liste sıralanır ve tekrarlar silinir.

Adım 4: Aday kümeden geçerli üçlüleri saymak

Sonra

$$a\le b\le c,\qquad a,b,c\in \mathcal{S}_r$$

olacak biçimde üçlüler gezilir. Liste sıralı olduğu için \(a+b\le c\) olduğunda iç döngü hemen durdurulur; çünkü üçgen eşitsizliği artık sağlanamaz. Kalan her üçlü için Adım 1'deki tam eşitlik sınanır. Geçen her üçlü \(T(r)\)'ye \(1\) katkı yapar.

Örnek olarak \(r=5\) alınırsa, \((m,n)=(2,1)\) çifti \(d=5\) ve paylar \(3,4\) verir. \(5\mid r\) olduğundan

$$2\cdot \frac{5}{5}\cdot 3=6,\qquad 2\cdot \frac{5}{5}\cdot 4=8,\qquad 2r=10$$

elde edilir. Böylece \((6,8,10)\) üçlüsü aday olur ve tam kontrol bu üçgenin çevrel yarıçapının gerçekten \(5\) olduğunu doğrular.

Kodun Çalışma Mantığı

Euler373Solver sınıfı iki temel ön hesap yapar: bölen üretimini hızlandıran bir en küçük asal bölen tablosu ve rasyonel sinüs adaylarını saklayan denominator_to_numerators yapısı. count_triangles_for_radius(r) fonksiyonu bu bilgilerle \(\mathcal{S}_r\) kümesini kurar, sıralı üçlüleri dener ve circumradius_equals(a,b,c,r) ile kesin doğrulama yapar.

Çevrel yarıçap özdeşliğindeki çarpımlar çok büyüdüğü için C++ çözümü özel bir 192 bitlik U192 çarpım taşıyıcısı kullanır. Dış toplamda her yarıçap bağımsız olduğundan iş parçacıkları arasında atomik bir sayaçla paylaştırma yapılır. Python ve Java dosyaları ise bu aynı C++ çözümünü derleyip çalıştıran köprülerdir; dolayısıyla üç dilin matematiği aynıdır.

Program, tam çözümden önce hızlı yöntemi \(r\le 60\) için kaba kuvvetle karşılaştırır ve şu kontrol değerlerini de sınar:

$$S(100)=4950,\qquad S(1200)=1653605.$$

Karmaşıklık Analizi

\(N\) yarıçap sınırı için en küçük asal bölen tablosu \(O(N\log\log N)\) zaman ve \(O(N)\) bellek gerektirir. Payda tablosu da \(m^2+n^2\le N\) koşulunu sağlayan primitif çiftler üzerinde bir kez oluşturulur.

Tek bir \(r\) için bölen üretimi, \(r\)'nin bölen sayısı kadar iş yapar; baskın maliyet deduplikasyon sonrası aday küme \(\mathcal{S}_r\) üzerinde üçlü taramadır. En kötü durumda bu bölüm \(O(|\mathcal{S}_r|^3)\) görünür, fakat yalnızca \(r\)'nin bölenlerinin kullanılması, tekrarların temizlenmesi ve \(a+b\le c\) anında erken çıkış yapılması pratikte maliyeti önemli ölçüde azaltır. Son toplam çok iş parçacıklı çalıştırılır.

Kaynaklar

  1. Problem sayfası: https://projecteuler.net/problem=373
  2. Çevrel çember ve genişletilmiş sinüs teoremi: Wikipedia — Çevrel çember
  3. Heron formülü: Wikipedia — Heron formülü
  4. Pisagor üçlüleri: Wikipedia — Pisagor üçlüsü

Resumen del Problema

Para cada radio circunscrito entero \(r\), sea \(T(r)\) el número de triángulos con lados enteros cuyo circunradio es exactamente \(r\). El programa calcula

$$S(N)=\sum_{r=1}^{N} r\,T(r),\qquad N=10^7.$$

Probar todos los triples \((a,b,c)\) hasta \(2r\) sería inviable. La idea real del código es generar primero solo las longitudes de lado que pueden aparecer con ese radio y después verificar exactamente las ternas candidatas.

Enfoque Matemático

Paso 1: Reescribir la condición del circunradio como una identidad entera

Si un triángulo tiene lados \(a,b,c\) y área \(\Delta\), la fórmula de Herón dice

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

Además, el circunradio cumple

$$R=\frac{abc}{4\Delta}.$$

Al elevar al cuadrado y eliminar \(\Delta\), obtenemos

$$a^2b^2c^2=R^2(a+b+c)(a+b-c)(a-b+c)(-a+b+c).$$

Esa es exactamente la igualdad que usa el solver. Así se evita cualquier error de coma flotante: decidir si el circunradio vale \(r\) se convierte en comparar dos productos enteros.

Paso 2: Obtener todas las longitudes posibles a partir de senos racionales

Si \(a\) está opuesto al ángulo \(A\), la ley extendida de los senos da

$$a=2r\sin A.$$

Como \(a\) y \(r\) son enteros, \(\sin A=a/(2r)\) es racional. Los puntos racionales del círculo unitario se parametrizan mediante ternas pitagóricas primitivas: para enteros coprimos \(m>n\ge 1\) de paridad opuesta,

$$\left(\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right)$$

está sobre \(x^2+y^2=1\). Por tanto, todo seno racional en \((0,1)\) puede escribirse como

$$\sin A\in\left\{\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right\}.$$

Si llamamos \(d=m^2+n^2\) al denominador y \(u\) a uno de los dos numeradores, entonces

$$a=2r\frac{u}{d}.$$

Por lo tanto, \(a\) es entero siempre que \(d\mid r\). El caso especial \(\sin A=1\) no aparece con un par finito \((m,n)\), así que el código añade explícitamente el diámetro \(a=2r\).

Paso 3: Precalcular denominadores útiles y luego mirar solo los divisores de \(r\)

La implementación en C++ construye una tabla indexada por \(d\le N\). Para cada par primitivo \((m,n)\) con \(d=m^2+n^2\le N\), guarda

$$u\in\{m^2-n^2,\ 2mn\}.$$

Para un radio fijo \(r\), solo pueden producir lados enteros aquellos denominadores que dividen a \(r\). Si \(r=kd\), cada numerador almacenado genera

$$a=2ku.$$

Por eso el solver primero factoriza \(r\) con una criba de menor factor primo, enumera todos los divisores de \(r\) y consulta únicamente esas entradas. El conjunto de candidatos queda

$$\mathcal{S}_r=\{2r\}\cup \left\{2\frac{r}{d}u : d\mid r,\ u\in U(d)\right\},$$

donde \(U(d)\) es la lista precalculada de numeradores asociada a \(d\). Después se ordena y se eliminan duplicados, porque diferentes parametrizaciones pueden producir la misma longitud.

Paso 4: Enumerar ternas válidas y confirmar el radio exacto

Una vez construido \(\mathcal{S}_r\), el programa recorre

$$a\le b\le c,\qquad a,b,c\in \mathcal{S}_r.$$

El orden permite una poda inmediata: si \(a+b\le c\), la desigualdad triangular falla y el bucle interior puede terminar. Para cada triple restante se aplica la identidad exacta del Paso 1. Cada triple que la satisface incrementa \(T(r)\).

Un ejemplo sencillo es \(r=5\). El par primitivo \((2,1)\) produce \(d=5\) y numeradores \(3\) y \(4\). Como \(5\mid r\), aparecen los lados

$$2\cdot \frac{5}{5}\cdot 3=6,\qquad 2\cdot \frac{5}{5}\cdot 4=8,\qquad 2r=10.$$

Así surge el triple \((6,8,10)\), y la comprobación exacta confirma que su circunradio es \(5\).

Cómo funciona el código

La clase Euler373Solver precalcula dos estructuras: un arreglo de menor factor primo para obtener divisores rápidamente y una tabla denominator_to_numerators para las razones trigonométricas racionales. El método count_triangles_for_radius(r) construye \(\mathcal{S}_r\), enumera ternas ordenadas y llama a circumradius_equals(a,b,c,r) para la verificación exacta.

Como los productos de la identidad pueden superar con holgura los 64 bits, la versión en C++ usa un acumulador manual de 192 bits llamado U192. La suma exterior sobre los radios es independiente, así que el programa la paraleliza repartiendo radios entre varios hilos mediante un contador atómico. Los archivos de Python y Java solo sirven de puente para compilar y ejecutar este mismo solver de C++, de modo que la lógica matemática es exactamente la misma en los tres casos.

Antes de resolver el límite final, el programa contrasta el método optimizado con fuerza bruta para \(r\le 60\) y verifica además

$$S(100)=4950,\qquad S(1200)=1653605.$$

Análisis de Complejidad

Si \(N\) es el límite de radios, construir la criba de menor factor primo cuesta \(O(N\log\log N)\) tiempo y \(O(N)\) memoria. La tabla de denominadores se genera una sola vez recorriendo pares primitivos con \(m^2+n^2\le N\).

Para un radio concreto \(r\), enumerar divisores cuesta lo que cuesta la estructura multiplicativa de \(r\), mientras que el trabajo dominante es la exploración cúbica sobre el conjunto deduplicado \(\mathcal{S}_r\). En el peor caso esa fase es \(O(|\mathcal{S}_r|^3)\), pero en la práctica se reduce mucho porque solo se consideran denominadores divisores de \(r\), se eliminan repeticiones y la condición \(a+b\le c\) corta pronto muchos recorridos. La suma total sobre todos los radios se ejecuta en paralelo.

Referencias

  1. Página del problema: https://projecteuler.net/problem=373
  2. Circunradio y ley extendida de los senos: Wikipedia — Circunferencia circunscrita
  3. Fórmula de Herón: Wikipedia — Fórmula de Herón
  4. Ternas pitagóricas: Wikipedia — Terna pitagórica

问题概述

对每个整数外接圆半径 \(r\),记 \(T(r)\) 为外接圆半径恰好等于 \(r\) 的整数边三角形个数。程序要求计算

$$S(N)=\sum_{r=1}^{N} r\,T(r),\qquad N=10^7.$$

如果对每个 \(r\) 都把所有边长三元组 \((a,b,c)\) 枚举到 \(2r\),代价会完全失控。真正的做法是先找出在固定半径下可能出现的边长,再对这些候选三元组做精确判定。

数学方法

步骤 1:把外接圆半径条件改写成精确整数恒等式

设三角形边长为 \(a,b,c\),面积为 \(\Delta\)。由海伦公式可得

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

外接圆半径满足

$$R=\frac{abc}{4\Delta}.$$

平方并消去 \(\Delta\) 以后,得到

$$a^2b^2c^2=R^2(a+b+c)(a+b-c)(a-b+c)(-a+b+c).$$

这正是求解器使用的判定式。它完全避免浮点误差,因为“外接圆半径是否等于 \(r\)”被转化成两个整数乘积是否相等。

步骤 2:用有理正弦参数化固定半径下可能出现的边长

若边 \(a\) 对应角 \(A\),由扩展正弦定理有

$$a=2r\sin A.$$

由于 \(a\) 和 \(r\) 都是整数,所以 \(\sin A=a/(2r)\) 必为有理数。单位圆上的有理点可以用原始勾股参数表示:当整数 \(m>n\ge 1\) 互素且奇偶性相反时,

$$\left(\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right)$$

是单位圆上的原始有理点。因此,\((0,1)\) 内的每个有理正弦值都可以写成

$$\sin A\in\left\{\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right\}.$$

记 \(d=m^2+n^2\),并把两个分子中的一个记为 \(u\),则

$$a=2r\frac{u}{d}.$$

只要 \(d\mid r\),边长 \(a\) 就一定是整数。特殊值 \(\sin A=1\) 不能由有限的 \((m,n)\) 产生,所以代码显式加入直径情况 \(a=2r\)。

步骤 3:先一次性预处理分母表,再只查看 \(r\) 的因子

C++ 实现构建了一个以 \(d\le N\) 为下标的表。对每个满足 \(d=m^2+n^2\le N\) 的原始参数对 \((m,n)\),程序把

$$u\in\{m^2-n^2,\ 2mn\}$$

记录到分母 \(d\) 对应的列表里。对于固定半径 \(r\),只有那些整除 \(r\) 的分母才会给出整数边。若 \(r=kd\),则每个已记录的分子都会产生候选边

$$a=2ku.$$

这就是为什么程序先用最小质因子表分解 \(r\),枚举 \(r\) 的全部因子,然后只查询这些因子对应的条目。于是候选边集合为

$$\mathcal{S}_r=\{2r\}\cup \left\{2\frac{r}{d}u : d\mid r,\ u\in U(d)\right\},$$

其中 \(U(d)\) 是分母 \(d\) 对应的分子列表。随后程序会排序并去重,因为不同参数对可能生成相同的整数边长。

步骤 4:从候选集合中枚举三元组并做精确验证

得到 \(\mathcal{S}_r\) 以后,程序枚举

$$a\le b\le c,\qquad a,b,c\in \mathcal{S}_r.$$

由于列表已经排好序,一旦出现 \(a+b\le c\),三角形不等式就失败,最内层循环可以立刻终止。对剩下的每个三元组,再应用步骤 1 的整数恒等式。通过检查的三元组都会给 \(T(r)\) 增加 \(1\)。

例如 \(r=5\) 时,原始参数 \((m,n)=(2,1)\) 给出 \(d=5\),分子为 \(3\) 和 \(4\)。因为 \(5\mid r\),候选边包含

$$2\cdot \frac{5}{5}\cdot 3=6,\qquad 2\cdot \frac{5}{5}\cdot 4=8,\qquad 2r=10.$$

于是 \((6,8,10)\) 会被枚举到,精确判定也确认它的外接圆半径确实是 \(5\)。

代码实现说明

Euler373Solver 类有两个核心预处理:最小质因子数组用于快速生成因子,denominator_to_numerators 表用于存放有理正弦候选。count_triangles_for_radius(r) 负责构造 \(\mathcal{S}_r\)、枚举有序三元组,并调用 circumradius_equals(a,b,c,r) 做精确判定。

由于判定式中的乘积非常大,C++ 版本专门实现了一个 192 位的整数累乘器 U192,否则普通 64 位类型会溢出。对所有半径求和时,各个 \(r\) 彼此独立,因此程序用原子计数器把半径分配给多个线程并行处理。Python 和 Java 文件都只是这个 C++ 求解器的桥接包装,所以三种语言在数学逻辑上完全一致。

在正式计算之前,程序先把优化算法与暴力枚举在 \(r\le 60\) 的范围内逐项比对,并额外验证

$$S(100)=4950,\qquad S(1200)=1653605.$$

复杂度分析

设 \(N\) 为最大半径。最小质因子筛的构建代价为 \(O(N\log\log N)\) 时间和 \(O(N)\) 内存。分母表只需对满足 \(m^2+n^2\le N\) 的原始参数对做一次预处理。

对单个半径 \(r\) 来说,因子枚举的成本取决于 \(r\) 的因子个数,而主要开销来自去重后的候选集合 \(\mathcal{S}_r\) 上的三重扫描。最坏情况下这一部分是 \(O(|\mathcal{S}_r|^3)\),但实际运行时会因为只考虑 \(r\) 的因子、去除重复边长以及利用 \(a+b\le c\) 的提前退出而显著减小。最终的总和在外层按半径并行计算。

参考资料

  1. 题目页面:https://projecteuler.net/problem=373
  2. 外接圆与扩展正弦定理:Wikipedia — 外接圆
  3. 海伦公式:Wikipedia — 海伦公式
  4. 勾股三元组:Wikipedia — 勾股數

Краткое описание

Для каждого целого радиуса описанной окружности \(r\) обозначим через \(T(r)\) число треугольников с целыми сторонами, у которых этот радиус равен ровно \(r\). Программа вычисляет

$$S(N)=\sum_{r=1}^{N} r\,T(r),\qquad N=10^7.$$

Полный перебор всех троек \((a,b,c)\) до \(2r\) был бы слишком дорогим. Поэтому решение сначала строит только те длины сторон, которые вообще могут возникнуть при фиксированном \(r\), а затем точно проверяет только эти кандидаты.

Математический подход

Шаг 1: записать условие на радиус как точное целочисленное равенство

Для треугольника со сторонами \(a,b,c\) и площадью \(\Delta\) формула Герона даёт

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

Радиус описанной окружности удовлетворяет соотношению

$$R=\frac{abc}{4\Delta}.$$

После возведения в квадрат и исключения \(\Delta\) получаем

$$a^2b^2c^2=R^2(a+b+c)(a+b-c)(a-b+c)(-a+b+c).$$

Именно это равенство использует программа. Никакой арифметики с плавающей точкой не нужно: вопрос о том, равен ли радиус \(r\), сводится к сравнению двух целых произведений.

Шаг 2: описать все возможные стороны через рациональные значения синуса

Если сторона \(a\) лежит напротив угла \(A\), то из расширенной теоремы синусов следует

$$a=2r\sin A.$$

Так как \(a\) и \(r\) целые, \(\sin A=a/(2r)\) рационален. Рациональные точки единичной окружности параметризуются примитивными пифагоровыми данными: для взаимно простых целых \(m>n\ge 1\) разной чётности

$$\left(\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right)$$

лежит на \(x^2+y^2=1\). Значит, любой рациональный синус из \((0,1)\) имеет вид

$$\sin A\in\left\{\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right\}.$$

Если обозначить \(d=m^2+n^2\) и \(u\) один из двух числителей, то

$$a=2r\frac{u}{d}.$$

Следовательно, \(a\) гарантированно целое, когда \(d\mid r\). Особое значение \(\sin A=1\) не возникает из конечной пары \((m,n)\), поэтому код добавляет случай диаметра \(a=2r\) отдельно.

Шаг 3: один раз предвычислить знаменатели и затем смотреть только на делители \(r\)

В C++-решении строится таблица по всем \(d\le N\). Для каждой примитивной пары \((m,n)\) с \(d=m^2+n^2\le N\) сохраняются допустимые числители

$$u\in\{m^2-n^2,\ 2mn\}.$$

Для фиксированного радиуса \(r\) целые стороны дают только те знаменатели, которые делят \(r\). Если \(r=kd\), то каждый сохранённый числитель порождает сторону

$$a=2ku.$$

Поэтому программа сначала раскладывает \(r\) с помощью массива наименьших простых делителей, перечисляет все делители числа \(r\) и обращается только к соответствующим элементам таблицы. Множество кандидатов имеет вид

$$\mathcal{S}_r=\{2r\}\cup \left\{2\frac{r}{d}u : d\mid r,\ u\in U(d)\right\},$$

где \(U(d)\) — список числителей, привязанный к знаменателю \(d\). Далее список сортируется и очищается от повторов, потому что разные параметризации могут давать одну и ту же длину стороны.

Шаг 4: перебрать тройки из множества кандидатов и точно проверить радиус

После построения \(\mathcal{S}_r\) программа рассматривает

$$a\le b\le c,\qquad a,b,c\in \mathcal{S}_r.$$

Отсортированный порядок даёт немедленную отсечку: как только \(a+b\le c\), неравенство треугольника нарушено, и внутренний цикл можно прервать. Для каждой оставшейся тройки проверяется точное равенство из шага 1. Каждое совпадение увеличивает \(T(r)\) на единицу.

Простейший пример: при \(r=5\) пара \((m,n)=(2,1)\) даёт \(d=5\) и числители \(3\) и \(4\). Поскольку \(5\mid r\), появляются стороны

$$2\cdot \frac{5}{5}\cdot 3=6,\qquad 2\cdot \frac{5}{5}\cdot 4=8,\qquad 2r=10.$$

Поэтому тройка \((6,8,10)\) попадает в перебор, а точная проверка подтверждает радиус \(5\).

Как работает код

Класс Euler373Solver имеет два главных предвычисления: массив наименьших простых делителей для быстрого перечисления делителей и таблицу denominator_to_numerators для рациональных синусов. Метод count_triangles_for_radius(r) строит \(\mathcal{S}_r\), перебирает упорядоченные тройки и вызывает circumradius_equals(a,b,c,r) для точной проверки.

Поскольку произведения в формуле получаются очень большими, C++-версия реализует собственный 192-битный аккумулятор U192. Внешняя сумма по радиусам независима, поэтому программа распараллеливает её, раздавая значения \(r\) потокам через атомарный счётчик. Файлы на Python и Java являются тонкими мостами к этому же C++-решению, так что математическая логика во всех трёх языках полностью совпадает.

Перед полным запуском программа сравнивает оптимизированный метод с brute force для \(r\le 60\) и дополнительно проверяет контрольные значения

$$S(100)=4950,\qquad S(1200)=1653605.$$

Анализ сложности

Если \(N\) — максимальный радиус, то построение массива наименьших простых делителей стоит \(O(N\log\log N)\) времени и \(O(N)\) памяти. Таблица знаменателей создаётся один раз при обходе примитивных пар с условием \(m^2+n^2\le N\).

Для одного радиуса \(r\) перечисление делителей зависит от его факторизации, а основной расход времени даёт перебор троек по очищенному множеству \(\mathcal{S}_r\). В худшем случае эта стадия имеет порядок \(O(|\mathcal{S}_r|^3)\), но на практике она заметно меньше из-за сильной арифметической фильтрации, удаления повторов и раннего выхода при \(a+b\le c\). Внешняя сумма по всем радиусам распараллелена.

Источники

  1. Страница задачи: https://projecteuler.net/problem=373
  2. Описанная окружность и расширенная теорема синусов: Wikipedia — Описанная окружность
  3. Формула Герона: Wikipedia — Формула Герона
  4. Пифагоровы тройки: Wikipedia — Пифагорова тройка

ملخص المسألة

لكل نصف قطر دائري محيط صحيح \(r\)، نرمز بـ \(T(r)\) لعدد المثلثات ذات الأضلاع الصحيحة التي يكون نصف قطر دائرتها المحيطة مساويًا تمامًا لـ \(r\). البرنامج يحسب

$$S(N)=\sum_{r=1}^{N} r\,T(r),\qquad N=10^7.$$

التعداد المباشر لكل ثلاثية أضلاع \((a,b,c)\) حتى \(2r\) غير عملي إطلاقًا. لذلك تبني الخوارزمية أولًا الأطوال التي يمكن أن تظهر فعلًا عند نصف قطر ثابت، ثم تختبر هذه المرشحات فقط باختبار صحيح ودقيق.

المنهج الرياضي

الخطوة 1: تحويل شرط نصف القطر إلى مساواة صحيحة بالكامل

إذا كانت أضلاع المثلث \(a,b,c\) ومساحته \(\Delta\)، فإن صيغة هيرون تعطي

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

كما أن نصف قطر الدائرة المحيطة يحقق

$$R=\frac{abc}{4\Delta}.$$

بالتربيع ثم حذف \(\Delta\) نحصل على

$$a^2b^2c^2=R^2(a+b+c)(a+b-c)(a-b+c)(-a+b+c).$$

هذه هي بالضبط المساواة التي يستخدمها الحل. وبهذا لا نحتاج إلى أي مقارنة عشرية؛ فالسؤال "هل نصف القطر يساوي \(r\)؟" يصبح مجرد مقارنة بين حاصلَي ضرب صحيحين.

الخطوة 2: وصف كل طول ضلع ممكن باستعمال قيم جيب نسبية

إذا كان الضلع \(a\) مقابل الزاوية \(A\)، فإن قانون الجيب الموسع يعطي

$$a=2r\sin A.$$

وبما أن \(a\) و\(r\) صحيحان، فإن \(\sin A=a/(2r)\) عدد نسبي. النقاط النسبية على دائرة الوحدة تُعطى ببارامترات فيثاغورس البدائية: إذا كان \(m>n\ge 1\) عددين صحيحين متباينين أوليًا ومختلفين في الفردية، فإن

$$\left(\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right)$$

نقطة نسبية على \(x^2+y^2=1\). لذلك يمكن كتابة كل قيمة جيب نسبية في المجال \((0,1)\) على الصورة

$$\sin A\in\left\{\frac{m^2-n^2}{m^2+n^2},\frac{2mn}{m^2+n^2}\right\}.$$

إذا كتبنا \(d=m^2+n^2\) للمقام و\(u\) لأحد البسطين، فإن

$$a=2r\frac{u}{d}.$$

ومن ثم يكون \(a\) عددًا صحيحًا كلما تحقق \(d\mid r\). أما الحالة الخاصة \(\sin A=1\) فلا تنتج من زوج منتهٍ \((m,n)\)، لذا يضيف الكود حالة القطر \(a=2r\) بشكل صريح.

الخطوة 3: تجهيز جدول للمقامات مرة واحدة ثم استخدام قواسم \(r\) فقط

يبني حل ++C جدولًا مفهرسًا بالمقام \(d\le N\). ولكل زوج بدائي \((m,n)\) يحقق \(d=m^2+n^2\le N\)، يخزن البرنامج

$$u\in\{m^2-n^2,\ 2mn\}.$$

عند نصف قطر ثابت \(r\)، لا تنتج أطوال صحيحة إلا من المقامات التي تقسم \(r\). فإذا كان \(r=kd\)، فإن كل بسط مخزن يولد الضلع

$$a=2ku.$$

لهذا السبب يفكك الحل العدد \(r\) باستعمال جدول أصغر عامل أولي، ثم يولد جميع قواسم \(r\)، ثم يراجع فقط مدخلات الجدول الموافقة لهذه القواسم. وهكذا تصبح مجموعة المرشحين

$$\mathcal{S}_r=\{2r\}\cup \left\{2\frac{r}{d}u : d\mid r,\ u\in U(d)\right\},$$

حيث \(U(d)\) هي قائمة البسوط المرتبطة بالمقام \(d\). بعد ذلك تُرتب القائمة وتُحذف التكرارات لأن أكثر من بارامتر يمكن أن يعطي الطول نفسه.

الخطوة 4: عد الثلاثيات الصالحة من مجموعة المرشحين

بعد بناء \(\mathcal{S}_r\)، يجرب البرنامج كل ثلاثية مرتبة

$$a\le b\le c,\qquad a,b,c\in \mathcal{S}_r.$$

وبسبب الترتيب يمكن إيقاف الحلقة الداخلية فور تحقق \(a+b\le c\)، لأن متباينة المثلث تفشل من تلك النقطة. ولكل ثلاثية متبقية يطبق البرنامج المساواة الصحيحة من الخطوة 1. وكل ثلاثية تنجح تضيف \(1\) إلى \(T(r)\).

مثال واضح هو \(r=5\). الزوج البدائي \((2,1)\) يعطي \(d=5\) والبسطين \(3\) و\(4\). ولأن \(5\mid r\)، تظهر الأضلاع

$$2\cdot \frac{5}{5}\cdot 3=6,\qquad 2\cdot \frac{5}{5}\cdot 4=8,\qquad 2r=10.$$

إذًا الثلاثية \((6,8,10)\) تدخل في الفحص، والاختبار الدقيق يؤكد أن نصف قطر دائرتها المحيطة يساوي فعلًا \(5\).

كيف يعمل الكود

تُجري الفئة Euler373Solver تجهيزين أساسيين: جدولًا لأصغر عامل أولي لتوليد القواسم بسرعة، وجدول denominator_to_numerators لتخزين مرشحات الجيب النسبي. ثم تبني الدالة count_triangles_for_radius(r) المجموعة \(\mathcal{S}_r\)، وتعد الثلاثيات المرتبة، وتستدعي circumradius_equals(a,b,c,r) للتحقق النهائي.

ولأن جداءات المساواة كبيرة جدًا، تستعمل نسخة ++C نوعًا يدويًا بطول 192 بت اسمه U192. أما الجمع الخارجي على جميع أنصاف الأقطار فهو مستقل بطبيعته، لذلك يوزع البرنامج القيم على عدة خيوط باستعمال عداد ذري. وملفا Python وJava ليسا إلا جسرين لتجميع هذا الحل نفسه وتشغيله، لذا فالتفاصيل الرياضية والتنفيذية متطابقة في اللغات الثلاث.

قبل الحساب النهائي، يقارن البرنامج الطريقة السريعة بالتعداد المباشر عندما \(r\le 60\)، كما يتحقق من القيمتين

$$S(100)=4950,\qquad S(1200)=1653605.$$

تحليل التعقيد

إذا كان \(N\) هو الحد الأعلى لنصف القطر، فإن بناء جدول أصغر عامل أولي يكلف \(O(N\log\log N)\) زمنًا و\(O(N)\) ذاكرة. أما جدول المقامات فيُبنى مرة واحدة فقط بالمرور على الأزواج البدائية التي تحقق \(m^2+n^2\le N\).

بالنسبة إلى نصف قطر واحد \(r\)، فإن توليد القواسم يعتمد على البنية الضربية للعدد، بينما تأتي الكلفة الأساسية من فحص ثلاثيات مجموعة المرشحين بعد حذف التكرار. في أسوأ الأحوال تكون هذه المرحلة \(O(|\mathcal{S}_r|^3)\)، لكن الكلفة العملية تنخفض كثيرًا لأننا لا نستخدم إلا المقامات القاسمة لـ \(r\)، ونحذف القيم المكررة، ونوقف البحث مبكرًا عندما يتحقق \(a+b\le c\). كما أن الجمع النهائي على جميع القيم \(r\) ينفذ بالتوازي.

مراجع

  1. صفحة المسألة: https://projecteuler.net/problem=373
  2. الدائرة المحيطة وقانون الجيب الموسع: Wikipedia — دائرة محيطة
  3. صيغة هيرون: Wikipedia — صيغة هيرون
  4. الثلاثيات الفيثاغورية: Wikipedia — ثلاثي فيثاغورس