Problem Summary

We want to count integer-sided triangles \((a,b,c)\) with \(a \le b \le c\), perimeter \(a+b+c \le P\), and

$$a^2+b^2=c^2+1.$$

For the Project Euler instance, \(P=25{,}000{,}000\). The equation says that the triangle is exactly one unit away from being right-angled, so a brute-force search over all triples is completely impractical.

The successful idea is to fix the smallest side \(a\), rewrite the equation as a factorization of \(a^2-1\), and count only the divisor pairs that can still satisfy the perimeter and ordering constraints.

Mathematical Approach

The central observation is that once \(a\) is fixed, the other two sides can be recovered from a factor pair of \(a^2-1\). That turns the problem from a quadratic search in \((b,c)\) into a bounded divisor-counting problem.

Fix the smallest side first

Because \(a \le b \le c\), every valid triangle satisfies

$$3a \le a+b+c \le P.$$

Therefore

$$1 \le a \le \left\lfloor \frac{P}{3}\right\rfloor.$$

The implementations loop over this range of possible smallest sides and solve the rest of the triangle from there.

Rewrite the equation as a product

Start from

$$a^2+b^2=c^2+1,$$

which can be rearranged as

$$c^2-b^2=a^2-1.$$

Now define

$$u=b+c,\qquad v=c-b.$$

Then \(u>0\), \(v\ge 0\), and

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

Conversely, once a factor pair \((u,v)\) is known, the two larger sides are

$$b=\frac{u-v}{2},\qquad c=\frac{u+v}{2}.$$

So for a fixed \(a\), every admissible triangle corresponds to a divisor pair of \(a^2-1\) with the parity condition

$$u\equiv v \pmod 2,$$

because \(b\) and \(c\) must be integers.

For \(a \ge 2\), we have \(a^2-1>0\), so \(v>0\). Then each valid choice of the smaller factor \(v\) determines \(u=(a^2-1)/v\), and therefore determines \((b,c)\) uniquely.

Turn the triangle conditions into bounds on one divisor

The perimeter becomes especially simple in the new variables:

$$a+b+c=a+u.$$

Hence the perimeter bound is

$$u \le P-a.$$

Since \(u=(a^2-1)/v\), this gives the lower bound

$$v \ge v_{\min}=\left\lceil\frac{a^2-1}{P-a}\right\rceil.$$

Very small divisors make \(u\) too large, so they cannot contribute.

The ordering condition \(a \le b\) becomes

$$a \le \frac{u-v}{2}\qquad\Longleftrightarrow\qquad u \ge v+2a.$$

Substituting \(u=(a^2-1)/v\) yields

$$v^2+2av \le a^2-1.$$

Solving this quadratic inequality gives an upper bound for the smaller factor:

$$v \le v_{\max}=\left\lfloor \sqrt{2a^2-1}-a\right\rfloor.$$

Therefore the search for \(v\) is completely localized:

$$v_{\min} \le v \le v_{\max},\qquad v \mid (a^2-1).$$

Only divisors of \(a^2-1\) inside this interval need to be tested.

Once this upper bound is enforced, the strict triangle inequality no longer needs a separate filter. Indeed,

$$a+b-c=a-v,$$

and the bound above implies \(v<a\). So every divisor that survives the bound and parity checks already yields a genuine triangle with \(a \le b \le c\).

The exceptional family \(a=1\)

When \(a=1\), the product \(a^2-1\) vanishes, so the divisor parameterization above is not the right tool. The original equation becomes

$$1+b^2=c^2+1,$$

which immediately simplifies to \(b=c\). So every triangle in this family has the form

$$ (1,b,b).$$

The perimeter condition is then

$$1+2b \le P,$$

so this case contributes exactly

$$\left\lfloor \frac{P-1}{2}\right\rfloor$$

solutions and is counted separately before the main divisor loop.

Worked example

Take \(a=5\). Then

$$a^2-1=24.$$

The upper bound from \(a \le b\) is

$$v_{\max}=\left\lfloor \sqrt{49}-5\right\rfloor=2.$$

So only the divisors \(v=1\) and \(v=2\) are even worth examining.

For \(v=1\), we get \(u=24\), but \(u-v=23\) is odd, so \(b\) and \(c\) would not be integers. For \(v=2\), we get \(u=12\), hence

$$b=\frac{12-2}{2}=5,\qquad c=\frac{12+2}{2}=7.$$

This produces the valid almost-right triangle \((5,5,7)\), and indeed

$$5^2+5^2=7^2+1.$$

The full algorithm repeats exactly this bounded divisor test for every \(a \le P/3\).

How the Code Works

The C++, Python, and Java implementations all follow the same mathematical plan. They first set \(a_{\max}=\lfloor P/3\rfloor\), add the separate contribution from \(a=1\), and build a smallest-prime-factor table up to \(a_{\max}+1\). That range is enough because the implementations never factor \(a^2-1\) directly. Instead they factor \(a-1\) and \(a+1\), then combine those prime exponents to recover the factorization of \((a-1)(a+1)=a^2-1\).

For each fixed \(a \ge 2\), the implementation computes \(v_{\min}\) from the perimeter bound and an initial floating-point estimate of \(v_{\max}\) from \(\sqrt{2a^2-1}-a\). Because floating-point rounding can miss the true floor by one, that estimate is corrected with exact integer inequalities before any counting starts. After that, the implementation generates only those divisors of \(a^2-1\) that do not exceed \(v_{\max}\), and each candidate \(v\) is checked against the four remaining conditions: \(v \ge v_{\min}\), correct parity, \(u \ge v+2a\), and \(a+u \le P\).

The work splits naturally across independent ranges of \(a\). The C++ and Java implementations distribute consecutive chunks of \(a\)-values to worker threads, while the Python implementation uses the same chunking idea with worker processes when that helps and falls back to a serial pass otherwise. Each worker needs only small temporary factor and divisor buffers, so the main shared structure is the prime-factor table.

Complexity Analysis

The smallest-prime-factor table up to \(a_{\max}+1=\lfloor P/3\rfloor+1\) costs \(O(P)\) time and \(O(P)\) memory up to constant factors. After that, the work for one fixed \(a\) is dominated by factoring \(a-1\) and \(a+1\) and by generating the admissible divisors of \(a^2-1\).

A convenient summary of the total runtime is

$$O\!\left(P+\sum_{a\le P/3}\tau(a^2-1)\right),$$

where \(\tau(n)\) is the divisor-counting function. This matches the structure of the implementations: the sieve is linear-size preprocessing, and the main loop spends its time on the divisor structure of \(a^2-1\). Parallel execution improves wall-clock time but does not change the asymptotic bound. Extra memory beyond the sieve is only small thread-local or process-local working storage.

Footnotes and References

  1. Problem page: Project Euler 223 - Almost Right-angled Triangles I
  2. Integer triangles: Wikipedia - Integer triangle
  3. Diophantine equations: Wikipedia - Diophantine equation
  4. Difference of two squares: Wikipedia - Difference of two squares
  5. Fast factorization with a smallest-prime-factor sieve: cp-algorithms - Integer factorization

Problemzusammenfassung

Gesucht ist die Anzahl ganzzahliger Dreiecke \((a,b,c)\) mit \(a \le b \le c\), Umfang \(a+b+c \le P\) und

$$a^2+b^2=c^2+1.$$

Im eigentlichen Project-Euler-Fall gilt \(P=25{,}000{,}000\). Die Gleichung beschreibt Dreiecke, die genau um 1 vom rechtwinkligen Fall abweichen, daher ist ein naiver Durchlauf über alle Tripel viel zu groß.

Die Lösung fixiert stattdessen zuerst die kleinste Seite \(a\), schreibt die Gleichung als Faktorisierung von \(a^2-1\) um und zählt nur noch diejenigen Teilerpaare, die überhaupt noch mit Umfang und Seitenordnung vereinbar sind.

Mathematischer Ansatz

Die entscheidende Beobachtung ist: Ist \(a\) fest, dann lassen sich \(b\) und \(c\) aus einem Faktorpärchen von \(a^2-1\) rekonstruieren. Damit wird aus einer quadratischen Suche in \((b,c)\) eine kontrollierte Teilerzählung.

Zuerst die kleinste Seite fixieren

Aus \(a \le b \le c\) folgt für jedes gültige Dreieck

$$3a \le a+b+c \le P.$$

Also gilt

$$1 \le a \le \left\lfloor \frac{P}{3}\right\rfloor.$$

Die Implementierungen laufen genau über diesen Bereich möglicher kleinster Seiten und leiten den Rest des Dreiecks daraus ab.

Die Gleichung in ein Produkt umformen

Aus

$$a^2+b^2=c^2+1$$

wird durch Umstellen

$$c^2-b^2=a^2-1.$$

Nun setzen wir

$$u=b+c,\qquad v=c-b.$$

Dann ist \(u>0\), \(v\ge 0\), und es gilt

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

Umgekehrt erhält man aus einem Faktorpärchen \((u,v)\) die beiden größeren Seiten durch

$$b=\frac{u-v}{2},\qquad c=\frac{u+v}{2}.$$

Für festes \(a\) entspricht also jedes zulässige Dreieck einem Teilerpärchen von \(a^2-1\) mit der Paritätsbedingung

$$u\equiv v \pmod 2,$$

denn \(b\) und \(c\) müssen ganzzahlig sein.

Für \(a \ge 2\) ist \(a^2-1>0\), also sogar \(v>0\). Dann bestimmt jede gültige Wahl des kleineren Faktors \(v\) eindeutig \(u=(a^2-1)/v\) und damit auch \((b,c)\).

Die Dreiecksbedingungen in Schranken für \(v\) übersetzen

Im neuen Koordinatensystem wird der Umfang besonders einfach:

$$a+b+c=a+u.$$

Daher ist die Umfangsbedingung

$$u \le P-a.$$

Mit \(u=(a^2-1)/v\) folgt daraus die untere Schranke

$$v \ge v_{\min}=\left\lceil\frac{a^2-1}{P-a}\right\rceil.$$

Zu kleine Teiler machen also \(u\) zu groß und scheiden sofort aus.

Die Ordnungsbedingung \(a \le b\) wird zu

$$a \le \frac{u-v}{2}\qquad\Longleftrightarrow\qquad u \ge v+2a.$$

Nach Einsetzen von \(u=(a^2-1)/v\) erhält man

$$v^2+2av \le a^2-1.$$

Diese quadratische Ungleichung liefert die obere Schranke

$$v \le v_{\max}=\left\lfloor \sqrt{2a^2-1}-a\right\rfloor.$$

Damit ist die Suche nach \(v\) vollständig lokalisiert:

$$v_{\min} \le v \le v_{\max},\qquad v \mid (a^2-1).$$

Nur Teiler von \(a^2-1\) in diesem Intervall können überhaupt beitragen.

Ist diese Schranke einmal durchgesetzt, braucht die strikte Dreiecksungleichung keinen eigenen Test mehr. Denn

$$a+b-c=a-v,$$

und aus der oberen Schranke folgt \(v<a\). Jeder Teiler, der die Schranken- und Paritätsprüfungen besteht, erzeugt also automatisch ein echtes Dreieck mit \(a \le b \le c\).

Die Ausnahmefamilie \(a=1\)

Für \(a=1\) verschwindet das Produkt \(a^2-1\), daher ist die obige Teilerparametrisierung nicht passend. Die Ursprungsgleichung wird zu

$$1+b^2=c^2+1,$$

also unmittelbar zu \(b=c\). Alle Dreiecke dieser Familie haben die Form

$$ (1,b,b).$$

Die Umfangsbedingung lautet dann

$$1+2b \le P,$$

und diese Familie liefert genau

$$\left\lfloor \frac{P-1}{2}\right\rfloor$$

Lösungen. Deshalb wird sie vor der eigentlichen Teilerschleife separat addiert.

Durchgerechnetes Beispiel

Nehmen wir \(a=5\). Dann ist

$$a^2-1=24.$$

Die obere Schranke aus \(a \le b\) ist

$$v_{\max}=\left\lfloor \sqrt{49}-5\right\rfloor=2.$$

Also müssen nur noch die Teiler \(v=1\) und \(v=2\) betrachtet werden.

Für \(v=1\) gilt \(u=24\), aber \(u-v=23\) ist ungerade, also wären \(b\) und \(c\) nicht ganzzahlig. Für \(v=2\) erhält man \(u=12\) und damit

$$b=\frac{12-2}{2}=5,\qquad c=\frac{12+2}{2}=7.$$

Das ergibt das gültige fast rechtwinklige Dreieck \((5,5,7)\), und tatsächlich gilt

$$5^2+5^2=7^2+1.$$

Genau diesen beschränkten Teilertest führt der Algorithmus für jedes \(a \le P/3\) aus.

Wie der Code arbeitet

Die C++-, Python- und Java-Implementierungen folgen derselben mathematischen Struktur. Zuerst setzen sie \(a_{\max}=\lfloor P/3\rfloor\), addieren den separaten Beitrag von \(a=1\) und bauen eine Tabelle des kleinsten Primteilers bis \(a_{\max}+1\) auf. Diese Reichweite genügt, weil nicht \(a^2-1\) direkt faktorisiert wird. Stattdessen werden \(a-1\) und \(a+1\) faktorisiert, und ihre Primexponenten werden anschließend zu der Faktorisierung von \((a-1)(a+1)=a^2-1\) zusammengeführt.

Für jedes feste \(a \ge 2\) berechnet die Implementierung \(v_{\min}\) aus der Umfangsbedingung und eine erste Gleitkomma-Näherung für \(v_{\max}\) aus \(\sqrt{2a^2-1}-a\). Da Rundungsfehler um eins danebenliegen können, wird diese Schätzung vor dem Zählen noch mit exakten ganzzahligen Ungleichungen korrigiert. Danach werden nur noch die Teiler von \(a^2-1\) erzeugt, die höchstens \(v_{\max}\) sind, und jeder Kandidat \(v\) wird gegen die vier verbleibenden Bedingungen geprüft: \(v \ge v_{\min}\), richtige Parität, \(u \ge v+2a\) und \(a+u \le P\).

Die Arbeit lässt sich natürlich parallelisieren, weil verschiedene Werte von \(a\) unabhängig sind. Die C++- und Java-Implementierungen verteilen zusammenhängende Blöcke von \(a\)-Werten auf Threads. Die Python-Implementierung verwendet dieselbe Blockidee mit Prozessen, wenn sich das lohnt, und fällt sonst auf einen seriellen Durchlauf zurück. Pro Worker werden nur kleine temporäre Puffer für Faktoren und Teiler benötigt; die große gemeinsame Struktur ist die Primteiler-Tabelle.

Komplexitätsanalyse

Die Tabelle des kleinsten Primteilers bis \(a_{\max}+1=\lfloor P/3\rfloor+1\) benötigt \(O(P)\) Zeit und \(O(P)\) Speicher bis auf konstante Faktoren. Danach wird die Arbeit für ein festes \(a\) von der Faktorisierung von \(a-1\) und \(a+1\) sowie von der Erzeugung der zulässigen Teiler von \(a^2-1\) dominiert.

Als kompakte Zusammenfassung der Gesamtlaufzeit bietet sich

$$O\!\left(P+\sum_{a\le P/3}\tau(a^2-1)\right)$$

an, wobei \(\tau(n)\) die Anzahl der positiven Teiler bezeichnet. Das passt exakt zur Struktur der Implementierungen: Das Sieb ist lineare Vorverarbeitung, und die Hauptschleife lebt von der Teilerstruktur von \(a^2-1\). Parallelisierung verbessert die reale Laufzeit, ändert aber nichts an der asymptotischen Schranke. Zusätzlicher Speicher über das Sieb hinaus besteht nur aus kleinen thread-lokalen oder prozess-lokalen Arbeitsbereichen.

Fußnoten und Referenzen

  1. Problemseite: Project Euler 223 - Almost Right-angled Triangles I
  2. Ganzzahlige Dreiecke: Wikipedia - Integer triangle
  3. Diophantische Gleichungen: Wikipedia - Diophantine equation
  4. Differenz zweier Quadrate: Wikipedia - Difference of two squares
  5. Schnelle Faktorisierung mit SPF-Sieb: cp-algorithms - Integer factorization

Problem Özeti

Aranan şey, \(a \le b \le c\), \(a+b+c \le P\) ve

$$a^2+b^2=c^2+1$$

koşullarını sağlayan tam sayı kenarlı üçgenlerin sayısıdır. Project Euler sürümünde \(P=25{,}000{,}000\) alınır. Denklem, üçgenin dik üçgene tam 1 kadar uzak olduğunu söyler; bu yüzden bütün \((a,b,c)\) üçlülerini kaba kuvvetle dolaşmak pratik değildir.

Başarılı yaklaşım, önce en küçük kenar \(a\)'yı sabitlemek, sonra denklemi \(a^2-1\)'in bölenlerine dönüştürmek ve yalnızca çevre ile kenar sırası koşullarını geçebilecek bölen çiftlerini saymaktır.

Matematiksel Yaklaşım

Ana fikir şudur: \(a\) sabitlenince diğer iki kenar, \(a^2-1\)'in bir çarpan çiftinden geri elde edilebilir. Böylece \((b,c)\) üzerinde kuadratik bir arama yapmak yerine sınırlı bir bölen sayımı yapılır.

Önce en küçük kenarı sabitle

\(a \le b \le c\) olduğundan her geçerli üçgen için

$$3a \le a+b+c \le P$$

vardır. Dolayısıyla

$$1 \le a \le \left\lfloor \frac{P}{3}\right\rfloor.$$

Uygulamalar tam olarak bu aralıktaki en küçük kenar adaylarını dolaşır ve üçgenin geri kalanını bu seçimden üretir.

Denklemi bir çarpım biçimine dönüştür

Başlangıç denklemi

$$a^2+b^2=c^2+1$$

şöyle yeniden yazılır:

$$c^2-b^2=a^2-1.$$

Şimdi

$$u=b+c,\qquad v=c-b$$

tanımlarını yapalım. O zaman \(u>0\), \(v\ge 0\) ve

$$uv=(b+c)(c-b)=c^2-b^2=a^2-1$$

olur. Tersine, bir \((u,v)\) çarpan çifti biliniyorsa büyük iki kenar

$$b=\frac{u-v}{2},\qquad c=\frac{u+v}{2}$$

şeklinde bulunur. Demek ki sabit bir \(a\) için her geçerli üçgen, \(a^2-1\)'in bir bölen çiftiyle eşleşir; fakat \(b\) ve \(c\) tam sayı olacağı için şu parite koşulu gerekir:

$$u\equiv v \pmod 2.$$

\(a \ge 2\) olduğunda \(a^2-1>0\) olduğu için \(v>0\) da zorunludur. Böylece küçük çarpan \(v\) seçildiğinde \(u=(a^2-1)/v\) ve ardından \((b,c)\) tek biçimde belirlenir.

Üçgen koşullarını \(v\) için sınırlara çevir

Yeni değişkenlerde çevre çok sadeleşir:

$$a+b+c=a+u.$$

Bu yüzden çevre koşulu

$$u \le P-a$$

olur. \(u=(a^2-1)/v\) yazınca alt sınır elde ederiz:

$$v \ge v_{\min}=\left\lceil\frac{a^2-1}{P-a}\right\rceil.$$

Yani çok küçük bölenler \(u\)'yu fazla büyüttüğü için hemen elenir.

Kenar sırası koşulu \(a \le b\) ise

$$a \le \frac{u-v}{2}\qquad\Longleftrightarrow\qquad u \ge v+2a$$

eşdeğerliğine dönüşür. Burada \(u=(a^2-1)/v\) yazılırsa

$$v^2+2av \le a^2-1$$

bulunur. Bu kuadratik eşitsizlik küçük çarpan için üst sınır verir:

$$v \le v_{\max}=\left\lfloor \sqrt{2a^2-1}-a\right\rfloor.$$

Böylece arama aralığı tamamen belirlenmiş olur:

$$v_{\min} \le v \le v_{\max},\qquad v \mid (a^2-1).$$

Yalnızca bu aralıktaki bölenler denenmelidir.

Bu üst sınır uygulandıktan sonra sıkı üçgen eşitsizliği için ayrı bir denetime gerek kalmaz. Çünkü

$$a+b-c=a-v,$$

ve yukarıdaki sınır \(v<a\) sonucunu verir. Dolayısıyla sınır ve parite testlerinden geçen her bölen, otomatik olarak \(a \le b \le c\) sağlayan gerçek bir üçgen üretir.

\(a=1\) özel durumu

\(a=1\) için \(a^2-1=0\) olur; bu yüzden yukarıdaki bölen parametreleştirmesi doğrudan kullanılmaz. Asıl denklem

$$1+b^2=c^2+1$$

haline gelir ve buradan hemen \(b=c\) çıkar. Demek ki bu ailedeki bütün üçgenler

$$ (1,b,b)$$

biçimindedir. Çevre şartı

$$1+2b \le P$$

olduğundan bu özel aile tam olarak

$$\left\lfloor \frac{P-1}{2}\right\rfloor$$

adet çözüm verir. Bu sayı ana döngü başlamadan önce ayrı eklenir.

Çözümlü örnek

\(a=5\) alalım. O zaman

$$a^2-1=24.$$

\(a \le b\) koşulundan gelen üst sınır

$$v_{\max}=\left\lfloor \sqrt{49}-5\right\rfloor=2$$

olur. Yani yalnızca \(v=1\) ve \(v=2\) bölenlerine bakmak gerekir.

\(v=1\) için \(u=24\) bulunur; fakat \(u-v=23\) tek sayı olduğu için \(b\) ve \(c\) tam sayı olmaz. \(v=2\) için \(u=12\) elde edilir ve

$$b=\frac{12-2}{2}=5,\qquad c=\frac{12+2}{2}=7$$

çıkar. Böylece \((5,5,7)\) üçgeni elde edilir ve gerçekten

$$5^2+5^2=7^2+1$$

eşitliği sağlanır. Tam algoritma, \(a \le P/3\) olan her değer için bu sınırlı bölen testini tekrarlar.

Kod Nasıl Çalışır

C++, Python ve Java uygulamaları aynı matematiksel planı uygular. Önce \(a_{\max}=\lfloor P/3\rfloor\) belirlenir, \(a=1\) için gelen ayrı katkı eklenir ve \(a_{\max}+1\)'e kadar en küçük asal bölen tablosu hazırlanır. Bu aralık yeterlidir; çünkü uygulamalar doğrudan \(a^2-1\)'i çarpanlara ayırmaz. Onun yerine \(a-1\) ve \(a+1\) ayrı ayrı ayrıştırılır, sonra bu iki listenin üsleri birleştirilerek \((a-1)(a+1)=a^2-1\)'in asal çarpan yapısı elde edilir.

Her sabit \(a \ge 2\) için önce çevre sınırından \(v_{\min}\) hesaplanır, sonra \(\sqrt{2a^2-1}-a\) ifadesinden \(v_{\max}\) için kayan noktalı bir ilk tahmin alınır. Kayan nokta hesabı gerçek tabandan bir yukarı ya da bir aşağı sapabileceği için, sayım başlamadan önce bu tahmin tam sayı eşitsizlikleriyle düzeltilir. Ardından yalnızca \(v_{\max}\)'i aşmayan bölenler üretilir ve her \(v\) adayı dört şarttan geçirilir: \(v \ge v_{\min}\), doğru parite, \(u \ge v+2a\) ve \(a+u \le P\).

Farklı \(a\) değerleri birbirinden bağımsız olduğundan iş doğal biçimde paralelleşir. C++ ve Java sürümleri ardışık \(a\) bloklarını iş parçacıklarına dağıtır. Python sürümü de aynı bloklama fikrini süreç havuzuyla kullanır; faydalı değilse seri çalışmaya döner. Her işçi yalnızca küçük geçici bölen ve çarpan tamponlarına ihtiyaç duyar; büyük ortak yapı asal bölen tablosudur.

Karmaşıklık Analizi

\(a_{\max}+1=\lfloor P/3\rfloor+1\) noktasına kadar kurulan en küçük asal bölen tablosu, sabit çarpanlar göz ardı edildiğinde \(O(P)\) zaman ve \(O(P)\) bellek ister. Bundan sonra sabit bir \(a\) için maliyetin baskın kısmı, \(a-1\) ve \(a+1\)'i çarpanlara ayırmak ve \(a^2-1\)'in geçerli bölenlerini üretmektir.

Toplam çalışma süresi için uygun bir özet

$$O\!\left(P+\sum_{a\le P/3}\tau(a^2-1)\right)$$

şeklindedir; burada \(\tau(n)\) bölen sayısı fonksiyonudur. Bu ifade, uygulamaların gerçek yapısıyla uyuşur: elek doğrusal büyüklükte bir ön işlemdir ve ana döngü zamanı \(a^2-1\)'in bölen yapısında harcanır. Paralel çalışma duvar saati süresini azaltır ama asimptotik sınırı değiştirmez. Eleğin ötesindeki ek bellek, yalnızca işçi başına küçük çalışma alanlarından ibarettir.

Dipnotlar ve Kaynaklar

  1. Problem sayfası: Project Euler 223 - Almost Right-angled Triangles I
  2. Tam sayı kenarlı üçgenler: Wikipedia - Integer triangle
  3. Diofant denklemleri: Wikipedia - Diophantine equation
  4. İki karenin farkı: Wikipedia - Difference of two squares
  5. En küçük asal bölen eleğiyle hızlı çarpanlara ayırma: cp-algorithms - Integer factorization

Resumen del Problema

Queremos contar los triángulos de lados enteros \((a,b,c)\) con \(a \le b \le c\), perímetro \(a+b+c \le P\) y

$$a^2+b^2=c^2+1.$$

En la instancia de Project Euler, \(P=25{,}000{,}000\). La ecuación dice que el triángulo está a una sola unidad del caso rectángulo, de modo que un recorrido directo sobre todos los ternas es inviable.

La idea eficaz es fijar primero el lado menor \(a\), reescribir la ecuación como una factorización de \(a^2-1\) y contar solo los pares de divisores que todavía pueden satisfacer el perímetro y el orden \(a \le b \le c\).

Enfoque Matemático

La observación central es que, una vez fijado \(a\), los otros dos lados pueden recuperarse a partir de un par de factores de \(a^2-1\). Así, el problema deja de ser una búsqueda cuadrática en \((b,c)\) y pasa a ser una enumeración acotada de divisores.

Fijar primero el lado más pequeño

Como \(a \le b \le c\), cualquier triángulo válido cumple

$$3a \le a+b+c \le P.$$

Por tanto,

$$1 \le a \le \left\lfloor \frac{P}{3}\right\rfloor.$$

Las implementaciones recorren exactamente ese rango de posibles valores para el lado menor y deducen el resto del triángulo a partir de ahí.

Convertir la ecuación en un producto

Partimos de

$$a^2+b^2=c^2+1,$$

que se reordena como

$$c^2-b^2=a^2-1.$$

Ahora definimos

$$u=b+c,\qquad v=c-b.$$

Entonces \(u>0\), \(v\ge 0\), y

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

A la inversa, si conocemos un par de factores \((u,v)\), obtenemos los lados mayores mediante

$$b=\frac{u-v}{2},\qquad c=\frac{u+v}{2}.$$

Así, para un \(a\) fijo, cada triángulo admisible corresponde a un par de divisores de \(a^2-1\) con la condición de paridad

$$u\equiv v \pmod 2,$$

porque \(b\) y \(c\) deben ser enteros.

Cuando \(a \ge 2\), se tiene \(a^2-1>0\), luego \(v>0\). En ese caso, cada elección válida del factor pequeño \(v\) determina \(u=(a^2-1)/v\) y por tanto determina \((b,c)\) de forma única.

Traducir las restricciones del triángulo a cotas sobre \(v\)

El perímetro se vuelve especialmente simple en las nuevas variables:

$$a+b+c=a+u.$$

Por eso la restricción de perímetro es

$$u \le P-a.$$

Como \(u=(a^2-1)/v\), esto se transforma en la cota inferior

$$v \ge v_{\min}=\left\lceil\frac{a^2-1}{P-a}\right\rceil.$$

Los divisores demasiado pequeños hacen que \(u\) sea demasiado grande y se descartan enseguida.

La condición de orden \(a \le b\) equivale a

$$a \le \frac{u-v}{2}\qquad\Longleftrightarrow\qquad u \ge v+2a.$$

Sustituyendo \(u=(a^2-1)/v\), obtenemos

$$v^2+2av \le a^2-1.$$

Resolver esta desigualdad cuadrática da una cota superior para el factor pequeño:

$$v \le v_{\max}=\left\lfloor \sqrt{2a^2-1}-a\right\rfloor.$$

Por tanto, la búsqueda de \(v\) queda completamente localizada:

$$v_{\min} \le v \le v_{\max},\qquad v \mid (a^2-1).$$

Solo pueden contribuir los divisores de \(a^2-1\) que estén dentro de ese intervalo.

Una vez impuesta esa cota superior, la desigualdad estricta del triángulo ya no necesita una comprobación aparte. En efecto,

$$a+b-c=a-v,$$

y la cota anterior implica \(v<a\). Así, cualquier divisor que supera las pruebas de rango y paridad ya produce automáticamente un triángulo auténtico con \(a \le b \le c\).

La familia excepcional \(a=1\)

Cuando \(a=1\), el producto \(a^2-1\) se anula, así que la parametrización por divisores deja de ser la herramienta adecuada. La ecuación original se convierte en

$$1+b^2=c^2+1,$$

y de ahí se obtiene inmediatamente \(b=c\). Todos los triángulos de esta familia tienen la forma

$$ (1,b,b).$$

La condición sobre el perímetro es

$$1+2b \le P,$$

de modo que este caso aporta exactamente

$$\left\lfloor \frac{P-1}{2}\right\rfloor$$

soluciones y se cuenta por separado antes del bucle principal.

Ejemplo trabajado

Tomemos \(a=5\). Entonces

$$a^2-1=24.$$

La cota superior que sale de \(a \le b\) es

$$v_{\max}=\left\lfloor \sqrt{49}-5\right\rfloor=2.$$

Por lo tanto, solo merece la pena revisar los divisores \(v=1\) y \(v=2\).

Para \(v=1\), obtenemos \(u=24\), pero \(u-v=23\) es impar, así que \(b\) y \(c\) no serían enteros. Para \(v=2\), resulta \(u=12\), y entonces

$$b=\frac{12-2}{2}=5,\qquad c=\frac{12+2}{2}=7.$$

Eso produce el triángulo válido \((5,5,7)\), y en efecto

$$5^2+5^2=7^2+1.$$

El algoritmo completo repite exactamente esta prueba acotada de divisores para cada \(a \le P/3\).

Cómo Funciona el Código

Las implementaciones en C++, Python y Java siguen el mismo plan matemático. Primero fijan \(a_{\max}=\lfloor P/3\rfloor\), añaden la contribución separada del caso \(a=1\) y construyen una tabla del menor factor primo hasta \(a_{\max}+1\). Ese rango basta porque no se factoriza \(a^2-1\) de manera directa. En su lugar se factorizan \(a-1\) y \(a+1\), y luego se combinan sus exponentes primos para reconstruir la factorización de \((a-1)(a+1)=a^2-1\).

Para cada \(a \ge 2\), la implementación calcula \(v_{\min}\) a partir del límite de perímetro y obtiene una primera estimación en coma flotante de \(v_{\max}\) usando \(\sqrt{2a^2-1}-a\). Como esa estimación puede quedar desplazada en una unidad por redondeo, se corrige con desigualdades enteras exactas antes de empezar a contar. Después se generan solo los divisores de \(a^2-1\) que no exceden \(v_{\max}\), y cada candidato \(v\) se comprueba contra las cuatro condiciones restantes: \(v \ge v_{\min}\), paridad correcta, \(u \ge v+2a\) y \(a+u \le P\).

El trabajo se divide de forma natural porque distintos valores de \(a\) son independientes. Las implementaciones en C++ y Java reparten bloques consecutivos de valores de \(a\) entre hilos, mientras que la implementación en Python usa la misma idea de bloques con procesos cuando compensa y, si no, ejecuta el recorrido de forma serial. Cada trabajador solo necesita pequeños buffers temporales para factores y divisores; la estructura grande compartida es la tabla de factores primos.

Análisis de Complejidad

La tabla del menor factor primo hasta \(a_{\max}+1=\lfloor P/3\rfloor+1\) cuesta \(O(P)\) tiempo y \(O(P)\) memoria, salvo factores constantes. Después, el coste para un valor fijo de \(a\) está dominado por factorizar \(a-1\) y \(a+1\) y por generar los divisores admisibles de \(a^2-1\).

Una forma cómoda de resumir el tiempo total es

$$O\!\left(P+\sum_{a\le P/3}\tau(a^2-1)\right),$$

donde \(\tau(n)\) es la función que cuenta divisores. Esta expresión encaja con la estructura real de las implementaciones: la criba es un preproceso lineal en tamaño, y el bucle principal dedica su esfuerzo a la estructura de divisores de \(a^2-1\). La paralelización reduce el tiempo de reloj, pero no cambia la cota asintótica. La memoria adicional, aparte de la criba, es solo almacenamiento de trabajo pequeño por hilo o por proceso.

Notas y Referencias

  1. Página del problema: Project Euler 223 - Almost Right-angled Triangles I
  2. Triángulos enteros: Wikipedia - Integer triangle
  3. Ecuaciones diofánticas: Wikipedia - Diophantine equation
  4. Diferencia de cuadrados: Wikipedia - Difference of two squares
  5. Factorización rápida con criba de menor factor primo: cp-algorithms - Integer factorization

问题概述

要求统计满足 \(a \le b \le c\)、周长 \(a+b+c \le P\),并且

$$a^2+b^2=c^2+1$$

的整数边三角形个数。对 Project Euler 这道题来说,\(P=25{,}000{,}000\)。这个方程表示三角形只比直角三角形偏离了 1,因此如果直接枚举所有三元组 \((a,b,c)\),规模会大得无法接受。

可行的方法是先固定最短边 \(a\),再把方程改写成 \(a^2-1\) 的因子分解问题,只统计那些仍然可能满足周长限制和边长顺序限制的因子对。

数学方法

核心观察是:一旦 \(a\) 固定,另外两条边 \(b,c\) 就可以从 \(a^2-1\) 的一个因子对中恢复出来。这样问题就从对 \((b,c)\) 的二次搜索,变成了带上下界的约数枚举。

先固定最短边

由于 \(a \le b \le c\),任何合法三角形都满足

$$3a \le a+b+c \le P.$$

因此

$$1 \le a \le \left\lfloor \frac{P}{3}\right\rfloor.$$

实现就是在这个范围内枚举可能的最短边 \(a\),然后由它推导出整组三边。

把方程改写成乘积形式

$$a^2+b^2=c^2+1$$

出发,可以整理为

$$c^2-b^2=a^2-1.$$

现在定义

$$u=b+c,\qquad v=c-b.$$

那么 \(u>0\)、\(v\ge 0\),并且

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

反过来,如果知道一个因子对 \((u,v)\),就能恢复出另外两条边:

$$b=\frac{u-v}{2},\qquad c=\frac{u+v}{2}.$$

所以对于固定的 \(a\),每一个可行三角形都对应 \(a^2-1\) 的一个因子对,但还必须满足奇偶性条件

$$u\equiv v \pmod 2,$$

这样 \(b\) 和 \(c\) 才会是整数。

当 \(a \ge 2\) 时,\(a^2-1>0\),因此 \(v>0\)。此时只要选定较小的因子 \(v\),就能唯一确定 \(u=(a^2-1)/v\),进而唯一确定 \((b,c)\)。

把三角形约束转成对 \(v\) 的上下界

在新变量下,周长写成非常简单的形式:

$$a+b+c=a+u.$$

因此周长限制就是

$$u \le P-a.$$

再代入 \(u=(a^2-1)/v\),得到 \(v\) 的下界

$$v \ge v_{\min}=\left\lceil\frac{a^2-1}{P-a}\right\rceil.$$

这意味着太小的约数会让 \(u\) 过大,因而不可能满足周长条件。

边长顺序条件 \(a \le b\) 等价于

$$a \le \frac{u-v}{2}\qquad\Longleftrightarrow\qquad u \ge v+2a.$$

把 \(u=(a^2-1)/v\) 代入后,得到

$$v^2+2av \le a^2-1.$$

解这个二次不等式,就得到较小因子 \(v\) 的上界:

$$v \le v_{\max}=\left\lfloor \sqrt{2a^2-1}-a\right\rfloor.$$

于是 \(v\) 的搜索范围被完全限定为

$$v_{\min} \le v \le v_{\max},\qquad v \mid (a^2-1).$$

只有落在这个区间里的约数才值得检查。

一旦施加了这个上界,严格三角形不等式就不需要再单独处理了。因为

$$a+b-c=a-v,$$

而上面的约束已经推出 \(v<a\)。所以只要约数通过了区间和奇偶性检查,它自动给出一个真正的三角形,并且满足 \(a \le b \le c\)。

特殊情形 \(a=1\)

当 \(a=1\) 时,\(a^2-1=0\),上面的约数参数化就不再合适。原方程变成

$$1+b^2=c^2+1,$$

马上得到 \(b=c\)。因此这一族三角形全都形如

$$ (1,b,b).$$

周长条件变成

$$1+2b \le P,$$

所以这一部分恰好贡献

$$\left\lfloor \frac{P-1}{2}\right\rfloor$$

个解。实现会在主循环开始前把这部分单独加入答案。

具体例子

取 \(a=5\)。此时

$$a^2-1=24.$$

由 \(a \le b\) 导出的上界是

$$v_{\max}=\left\lfloor \sqrt{49}-5\right\rfloor=2.$$

因此只需要看 \(v=1\) 和 \(v=2\) 这两个约数。

当 \(v=1\) 时,\(u=24\),但 \(u-v=23\) 是奇数,所以 \(b,c\) 不是整数。 当 \(v=2\) 时,\(u=12\),于是

$$b=\frac{12-2}{2}=5,\qquad c=\frac{12+2}{2}=7.$$

这就得到合法三角形 \((5,5,7)\),并且确实满足

$$5^2+5^2=7^2+1.$$

完整算法就是对每个 \(a \le P/3\) 重复同样的有界约数测试。

代码如何工作

C++、Python 和 Java 实现遵循完全相同的数学框架。它们先设定 \(a_{\max}=\lfloor P/3\rfloor\),加入 \(a=1\) 这一特殊家族的贡献,然后建立一个到 \(a_{\max}+1\) 为止的最小质因子表。之所以只需要做到这个范围,是因为实现并不直接分解 \(a^2-1\)。它们先分别分解 \(a-1\) 和 \(a+1\),再把这两部分的质因子指数合并起来,从而恢复 \((a-1)(a+1)=a^2-1\) 的质因子分解。

对于每个固定的 \(a \ge 2\),实现先根据周长限制算出 \(v_{\min}\),再用 \(\sqrt{2a^2-1}-a\) 得到 \(v_{\max}\) 的浮点初值。由于浮点误差可能让这个值偏离真实下取整一个单位,程序会先用精确的整数不等式把它修正到正确位置,再开始计数。之后只生成那些不超过 \(v_{\max}\) 的约数,并对每个候选 \(v\) 依次检查四个剩余条件:\(v \ge v_{\min}\)、奇偶性正确、\(u \ge v+2a\)、以及 \(a+u \le P\)。

不同的 \(a\) 之间彼此独立,因此这部分工作天然可以并行。C++ 和 Java 实现把连续的 \(a\) 区间分给多个线程;Python 实现也采用同样的分块思想,在合适时使用进程池,否则就顺序执行。每个工作单元只需要很小的临时因子和约数缓冲区,所以真正的大型共享数据结构只有那张最小质因子表。

复杂度分析

建立到 \(a_{\max}+1=\lfloor P/3\rfloor+1\) 的最小质因子表,需要 \(O(P)\) 时间和 \(O(P)\) 空间,常数因子忽略不计。之后,对固定一个 \(a\) 来说,主要成本来自分解 \(a-1\) 与 \(a+1\),以及生成 \(a^2-1\) 的可行约数。

总运行时间可以方便地概括为

$$O\!\left(P+\sum_{a\le P/3}\tau(a^2-1)\right),$$

其中 \(\tau(n)\) 表示约数个数函数。这个表达式与实现结构完全一致:筛法是线性规模的预处理,主循环的主要工作量则集中在 \(a^2-1\) 的约数结构上。并行化只会改善实际耗时,不会改变渐近复杂度。除筛表之外,额外内存只包括很小的线程局部或进程局部工作区。

注释与参考资料

  1. 题目页面:Project Euler 223 - Almost Right-angled Triangles I
  2. 整数三角形:Wikipedia - Integer triangle
  3. 丢番图方程:Wikipedia - Diophantine equation
  4. 平方差分解:Wikipedia - Difference of two squares
  5. 利用最小质因子筛进行快速分解:cp-algorithms - Integer factorization

Краткое содержание задачи

Нужно посчитать треугольники с целыми сторонами \((a,b,c)\), для которых \(a \le b \le c\), периметр \(a+b+c \le P\), и выполняется

$$a^2+b^2=c^2+1.$$

В задаче Project Euler берется \(P=25{,}000{,}000\). Это семейство треугольников отличается от прямоугольных ровно на 1, поэтому прямой перебор всех троек \((a,b,c)\) слишком дорог.

Рабочая идея состоит в том, чтобы сначала зафиксировать наименьшую сторону \(a\), затем переписать уравнение как разложение числа \(a^2-1\) на множители и считать только те пары делителей, которые еще могут удовлетворить ограничению на периметр и условию порядка \(a \le b \le c\).

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

Главное наблюдение такое: после фиксации \(a\) стороны \(b\) и \(c\) восстанавливаются из одной пары множителей числа \(a^2-1\). Тем самым задача превращается не в квадратичный поиск по \((b,c)\), а в ограниченный перебор делителей.

Сначала фиксируем наименьшую сторону

Из условия \(a \le b \le c\) следует

$$3a \le a+b+c \le P.$$

Значит,

$$1 \le a \le \left\lfloor \frac{P}{3}\right\rfloor.$$

Именно по этому диапазону возможных значений наименьшей стороны и проходят реализации, а остальные стороны выводятся уже из выбранного \(a\).

Преобразование уравнения в задачу о множителях

Начинаем с

$$a^2+b^2=c^2+1,$$

что можно переписать как

$$c^2-b^2=a^2-1.$$

Теперь введем

$$u=b+c,\qquad v=c-b.$$

Тогда \(u>0\), \(v\ge 0\), и

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

Обратно, если известна пара множителей \((u,v)\), то две большие стороны равны

$$b=\frac{u-v}{2},\qquad c=\frac{u+v}{2}.$$

Следовательно, для фиксированного \(a\) каждый допустимый треугольник соответствует паре делителей числа \(a^2-1\), причем должна выполняться условие четности

$$u\equiv v \pmod 2,$$

иначе \(b\) и \(c\) не будут целыми.

Если \(a \ge 2\), то \(a^2-1>0\), а значит \(v>0\). Тогда каждый допустимый выбор меньшего множителя \(v\) однозначно задает \(u=(a^2-1)/v\), а вместе с ним и пару \((b,c)\).

Перевод ограничений треугольника в границы для \(v\)

В новых переменных периметр имеет особенно простой вид:

$$a+b+c=a+u.$$

Поэтому ограничение на периметр выглядит так:

$$u \le P-a.$$

Подставляя \(u=(a^2-1)/v\), получаем нижнюю границу

$$v \ge v_{\min}=\left\lceil\frac{a^2-1}{P-a}\right\rceil.$$

Слишком маленькие делители дают слишком большое \(u\), так что их можно сразу отбросить.

Условие порядка \(a \le b\) эквивалентно

$$a \le \frac{u-v}{2}\qquad\Longleftrightarrow\qquad u \ge v+2a.$$

Если теперь подставить \(u=(a^2-1)/v\), получится

$$v^2+2av \le a^2-1.$$

Решение этого квадратного неравенства дает верхнюю границу для меньшего множителя:

$$v \le v_{\max}=\left\lfloor \sqrt{2a^2-1}-a\right\rfloor.$$

Значит, поиск по \(v\) полностью локализован:

$$v_{\min} \le v \le v_{\max},\qquad v \mid (a^2-1).$$

Проверять нужно только делители \(a^2-1\), лежащие в этом промежутке.

После введения этой верхней границы строгое неравенство треугольника уже не требует отдельной проверки. Действительно,

$$a+b-c=a-v,$$

а из полученной оценки следует \(v<a\). Поэтому любой делитель, прошедший проверки диапазона и четности, автоматически задает настоящий треугольник с \(a \le b \le c\).

Особый случай \(a=1\)

При \(a=1\) произведение \(a^2-1\) равно нулю, поэтому описанная параметризация делителями здесь не подходит. Исходное уравнение превращается в

$$1+b^2=c^2+1,$$

откуда сразу следует \(b=c\). Значит, все треугольники этого семейства имеют вид

$$ (1,b,b).$$

Условие на периметр тогда равно

$$1+2b \le P,$$

и эта ветвь дает ровно

$$\left\lfloor \frac{P-1}{2}\right\rfloor$$

решений. Поэтому ее считают отдельно до основного цикла по делителям.

Разобранный пример

Возьмем \(a=5\). Тогда

$$a^2-1=24.$$

Верхняя граница, вытекающая из \(a \le b\), равна

$$v_{\max}=\left\lfloor \sqrt{49}-5\right\rfloor=2.$$

Значит, достаточно проверить только делители \(v=1\) и \(v=2\).

Для \(v=1\) получаем \(u=24\), но \(u-v=23\) нечетно, поэтому \(b\) и \(c\) не будут целыми. Для \(v=2\) имеем \(u=12\), а значит

$$b=\frac{12-2}{2}=5,\qquad c=\frac{12+2}{2}=7.$$

Получается допустимый треугольник \((5,5,7)\), и действительно

$$5^2+5^2=7^2+1.$$

Именно такую ограниченную проверку делителей алгоритм повторяет для каждого \(a \le P/3\).

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

Реализации на C++, Python и Java следуют одной и той же математической схеме. Сначала они устанавливают \(a_{\max}=\lfloor P/3\rfloor\), добавляют отдельный вклад случая \(a=1\) и строят таблицу наименьшего простого делителя до \(a_{\max}+1\). Этого диапазона достаточно, потому что число \(a^2-1\) не факторизуется напрямую. Вместо этого отдельно факторизуются \(a-1\) и \(a+1\), а затем их простые показатели объединяются, чтобы восстановить разложение \((a-1)(a+1)=a^2-1\).

Для каждого фиксированного \(a \ge 2\) вычисляется \(v_{\min}\) из ограничения на периметр и берется первоначальная вещественная оценка для \(v_{\max}\) по формуле \(\sqrt{2a^2-1}-a\). Поскольку округление с плавающей точкой может ошибиться на единицу, эта оценка затем поправляется точными целочисленными неравенствами. После этого генерируются только те делители числа \(a^2-1\), которые не превосходят \(v_{\max}\), и каждый кандидат \(v\) проходит четыре оставшиеся проверки: \(v \ge v_{\min}\), правильная четность, \(u \ge v+2a\) и \(a+u \le P\).

Работа естественно распараллеливается, потому что разные значения \(a\) независимы. Реализации на C++ и Java раздают последовательные блоки значений \(a\) рабочим потокам. Реализация на Python использует ту же идею блоков с рабочими процессами, когда это оправдано, и иначе выполняет проход последовательно. Каждому рабочему нужны только небольшие временные буферы для факторов и делителей; крупной общей структурой остается таблица простых делителей.

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

Построение таблицы наименьшего простого делителя до \(a_{\max}+1=\lfloor P/3\rfloor+1\) требует \(O(P)\) времени и \(O(P)\) памяти с точностью до постоянных множителей. После этого стоимость для одного фиксированного \(a\) определяется в основном факторизацией \(a-1\) и \(a+1\), а также генерацией допустимых делителей числа \(a^2-1\).

Удобная сводка общей трудоемкости такова:

$$O\!\left(P+\sum_{a\le P/3}\tau(a^2-1)\right),$$

где \(\tau(n)\) обозначает функцию числа делителей. Эта формула точно отражает структуру реализаций: решето является линейной по размеру предварительной обработкой, а основное время уходит на делительную структуру числа \(a^2-1\). Параллельное исполнение сокращает реальное время работы, но не меняет асимптотическую оценку. Дополнительная память сверх решета сводится к небольшим локальным рабочим буферам потоков или процессов.

Примечания и ссылки

  1. Страница задачи: Project Euler 223 - Almost Right-angled Triangles I
  2. Целочисленные треугольники: Wikipedia - Integer triangle
  3. Диофантовы уравнения: Wikipedia - Diophantine equation
  4. Разность квадратов: Wikipedia - Difference of two squares
  5. Быстрая факторизация через решето наименьших простых делителей: cp-algorithms - Integer factorization

ملخص المسألة

نريد عدّ المثلثات ذات الأضلاع الصحيحة \((a,b,c)\) التي تحقق \(a \le b \le c\)، ومحيطها \(a+b+c \le P\)، وتلبي المعادلة

$$a^2+b^2=c^2+1.$$

في نسخة Project Euler تكون \(P=25{,}000{,}000\). هذه المعادلة تعني أن المثلث يبتعد عن المثلث القائم بمقدار 1 فقط، ولذلك فإن الفحص المباشر لجميع الثلاثيات \((a,b,c)\) غير عملي تمامًا.

الفكرة الفعالة هي تثبيت الضلع الأصغر \(a\) أولًا، ثم إعادة كتابة المعادلة على صورة تحليل للعدد \(a^2-1\)، وبعد ذلك نعدّ فقط أزواج القواسم التي ما زال يمكنها أن تحقق شرط المحيط وترتيب الأضلاع.

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

الملاحظة الأساسية هي أنه بعد تثبيت \(a\)، يمكن استرجاع الضلعين \(b\) و\(c\) من زوج عوامل للعدد \(a^2-1\). وهكذا تتحول المسألة من بحث تربيعي في \((b,c)\) إلى تعداد مقيد للقواسم.

تثبيت الضلع الأصغر أولًا

بما أن \(a \le b \le c\)، فإن كل مثلث صالح يحقق

$$3a \le a+b+c \le P.$$

ومن ثم

$$1 \le a \le \left\lfloor \frac{P}{3}\right\rfloor.$$

لذلك تدور الخوارزمية على جميع القيم الممكنة للضلع الأصغر فقط، ثم تستنتج منها بقية أضلاع المثلث.

تحويل المعادلة إلى صورة جداء

نبدأ من

$$a^2+b^2=c^2+1,$$

ثم نعيد ترتيبها إلى

$$c^2-b^2=a^2-1.$$

الآن نعرّف

$$u=b+c,\qquad v=c-b.$$

عندئذ يكون \(u>0\) و\(v\ge 0\)، كما أن

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

وبالعكس، إذا عرفنا زوج العوامل \((u,v)\)، فإن الضلعين الأكبرين يساويان

$$b=\frac{u-v}{2},\qquad c=\frac{u+v}{2}.$$

إذن لكل \(a\) ثابت، يقابل كل مثلث مقبول زوج قواسم للعدد \(a^2-1\)، لكن يجب أيضًا أن يتحقق شرط الزوجية

$$u\equiv v \pmod 2,$$

حتى يكون \(b\) و\(c\) عددين صحيحين.

إذا كان \(a \ge 2\)، فإن \(a^2-1>0\)، وبالتالي \(v>0\). وعندها فإن اختيار العامل الأصغر \(v\) يحدد \(u=(a^2-1)/v\) ثم يحدد \((b,c)\) بصورة وحيدة.

تحويل قيود المثلث إلى حدود على \(v\)

في المتغيرات الجديدة يصبح المحيط بسيطًا جدًا:

$$a+b+c=a+u.$$

لذلك فإن شرط المحيط هو

$$u \le P-a.$$

ومع \(u=(a^2-1)/v\) نحصل على الحد الأدنى

$$v \ge v_{\min}=\left\lceil\frac{a^2-1}{P-a}\right\rceil.$$

أي أن القواسم الصغيرة جدًا تجعل \(u\) كبيرًا أكثر من اللازم، فتُستبعد فورًا.

أما شرط الترتيب \(a \le b\) فيكافئ

$$a \le \frac{u-v}{2}\qquad\Longleftrightarrow\qquad u \ge v+2a.$$

وبالتعويض عن \(u\) نحصل على

$$v^2+2av \le a^2-1.$$

وحل هذه المتباينة التربيعية يعطي حدًا أعلى للعامل الأصغر:

$$v \le v_{\max}=\left\lfloor \sqrt{2a^2-1}-a\right\rfloor.$$

وهكذا تصبح مساحة البحث عن \(v\) محددة تمامًا:

$$v_{\min} \le v \le v_{\max},\qquad v \mid (a^2-1).$$

فقط القواسم الواقعة داخل هذا المجال يمكن أن تنتج حلولًا.

وبعد فرض هذا الحد الأعلى، لا نحتاج إلى اختبار منفصل لمتباينة المثلث الصارمة. ذلك لأن

$$a+b-c=a-v,$$

ومن القيد السابق نستنتج \(v<a\). لذا فإن أي قاسم يمر باختباري المجال والزوجية يعطي تلقائيًا مثلثًا حقيقيًا يحقق \(a \le b \le c\).

الحالة الخاصة \(a=1\)

عندما يكون \(a=1\)، فإن \(a^2-1=0\)، ولذلك لا تكون بارامترة القواسم السابقة هي الأداة المناسبة. تصبح المعادلة الأصلية

$$1+b^2=c^2+1,$$

ومنها مباشرة \(b=c\). إذن جميع مثلثات هذه العائلة تأخذ الشكل

$$ (1,b,b).$$

ويصبح شرط المحيط

$$1+2b \le P,$$

فتعطي هذه الحالة بالضبط

$$\left\lfloor \frac{P-1}{2}\right\rfloor$$

حلًا، ولذلك تُحسب منفصلة قبل بدء الحلقة الرئيسية.

مثال محلول

لنأخذ \(a=5\). عندئذ

$$a^2-1=24.$$

والحد الأعلى الناتج من الشرط \(a \le b\) هو

$$v_{\max}=\left\lfloor \sqrt{49}-5\right\rfloor=2.$$

إذًا لا نحتاج إلى فحص إلا القاسمين \(v=1\) و\(v=2\).

إذا كان \(v=1\)، فإن \(u=24\)، لكن \(u-v=23\) فردي، ولذلك لن يكون \(b\) و\(c\) عددين صحيحين. أما إذا كان \(v=2\)، فنحصل على \(u=12\)، ومن ثم

$$b=\frac{12-2}{2}=5,\qquad c=\frac{12+2}{2}=7.$$

وهذا يعطي المثلث الصحيح \((5,5,7)\)، وبالفعل

$$5^2+5^2=7^2+1.$$

وتكرر الخوارزمية هذه التجربة نفسها، لكن بصورة منهجية، لكل \(a \le P/3\).

كيف تعمل الشفرة

تتبع تطبيقات C++ وPython وJava الخطة الرياضية نفسها. فهي تبدأ بتحديد \(a_{\max}=\lfloor P/3\rfloor\)، ثم تضيف المساهمة الخاصة بحالة \(a=1\)، ثم تبني جدولًا لأصغر عامل أولي حتى \(a_{\max}+1\). ويكفي هذا الحد لأن التنفيذ لا يحلل \(a^2-1\) مباشرة. بدلًا من ذلك، يُحلَّل كل من \(a-1\) و\(a+1\) على حدة، ثم تُدمج أسس العوامل الأولية للحصول على تحليل \((a-1)(a+1)=a^2-1\).

لكل قيمة ثابتة \(a \ge 2\)، تحسب الشفرة \(v_{\min}\) من شرط المحيط، ثم تأخذ تقديرًا أوليًا عائمًا لـ \(v_{\max}\) من \(\sqrt{2a^2-1}-a\). وبما أن التقريب العائم قد يخطئ بمقدار واحد، فإن هذا التقدير يُصحَّح قبل العد باستعمال متباينات صحيحة دقيقة. بعد ذلك تُولَّد فقط قواسم \(a^2-1\) التي لا تتجاوز \(v_{\max}\)، ويُفحص كل مرشح \(v\) وفق الشروط الأربعة المتبقية: \(v \ge v_{\min}\)، والزوجية الصحيحة، و\(u \ge v+2a\)، و\(a+u \le P\).

هذا العمل يقبل التوازي بطبيعته لأن القيم المختلفة لـ \(a\) مستقلة عن بعضها. توزع تطبيقتا C++ وJava كتلًا متتالية من قيم \(a\) على الخيوط، بينما يستخدم تطبيق Python الفكرة نفسها مع العمليات عندما يكون ذلك مفيدًا، ويعود إلى التنفيذ التسلسلي عند الحاجة. كل عامل يحتاج فقط إلى مخازن مؤقتة صغيرة للعوامل والقواسم، لذا فإن البنية الكبيرة المشتركة فعليًا هي جدول العوامل الأولية.

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

بناء جدول أصغر عامل أولي حتى \(a_{\max}+1=\lfloor P/3\rfloor+1\) يكلف \(O(P)\) زمنًا و\(O(P)\) ذاكرة مع تجاهل الثوابت. وبعد ذلك تصبح كلفة القيمة الواحدة من \(a\) محكومة أساسًا بتحليل \(a-1\) و\(a+1\) وتوليد القواسم المقبولة للعدد \(a^2-1\).

ويمكن تلخيص زمن التنفيذ الكلي على الصورة

$$O\!\left(P+\sum_{a\le P/3}\tau(a^2-1)\right),$$

حيث \(\tau(n)\) هي دالة عدد القواسم. هذا يطابق بنية التنفيذ بدقة: فالسير هو مرحلة تمهيدية خطية الحجم، والحلقة الرئيسية تقضي وقتها في بنية قواسم \(a^2-1\). أما التوازي فيحسن زمن التنفيذ الفعلي فقط، ولا يغير الرتبة التقاربية. والذاكرة الإضافية بعد جدول السير تقتصر على مساحات عمل صغيرة محلية للخيوط أو للعمليات.

حواشٍ ومراجع

  1. صفحة المسألة: Project Euler 223 - Almost Right-angled Triangles I
  2. المثلثات ذات الأضلاع الصحيحة: Wikipedia - Integer triangle
  3. المعادلات الديوفانتية: Wikipedia - Diophantine equation
  4. فرق مربعين: Wikipedia - Difference of two squares
  5. التحليل السريع باستخدام جدول أصغر عامل أولي: cp-algorithms - Integer factorization