Let \(d(n)\) denote the sum of the proper divisors of \(n\), meaning all positive divisors strictly smaller than \(n\). Two distinct positive integers \(a\) and \(b\) form an amicable pair when
$$d(a)=b,\qquad d(b)=a,\qquad a \ne b.$$
The original problem asks for the sum of all amicable numbers below \(10000\). The implementations are written for a general limit \(L\): they sum every \(a \lt L\) that belongs to an amicable pair. The partner \(b=d(a)\) may lie either below or above the limit; only \(a\) itself is required to be below \(L\).
The smallest example is the pair \((220,284)\). Its importance is not just historical. It already shows the exact two-step return that the algorithm must detect.
The solution uses two complementary descriptions of the same quantity \(d(n)\). A sieve computes every proper-divisor sum below the search limit at once, while a prime-factor formula handles the rarer case in which the partner value lies outside the precomputed range.
By definition,
$$d(n)=\sum_{\substack{m\mid n\\1\le m\lt n}} m=\sigma(n)-n,$$
where \(\sigma(n)\) is the sum of all positive divisors of \(n\). The amicable condition is therefore exactly the statement that \(a\) and \(b\) form a 2-cycle under the map \(d\), with the extra condition \(a\ne b\) excluding perfect numbers such as \(6\) and \(28\).
Factoring every number separately would work, but it would waste the shared structure of divisor sums. The implementations instead allocate an array for all values \(0,1,\dots,L-1\) and add each possible proper divisor to all of its multiples:
$$d(kq)\mathrel{+}=q \qquad \left(q=1,2,\dots,\left\lfloor\frac{L-1}{2}\right\rfloor,\ k\ge 2,\ kq\lt L\right).$$
The key invariant is that after the loop has processed a divisor \(q\), every number below \(L\) has already received every proper divisor at most \(q\), and it has received each such divisor exactly once. Because the inner loop starts at \(2q\), no number ever adds itself to its own proper-divisor sum.
The total work is governed by the harmonic series:
$$\sum_{q=1}^{\lfloor(L-1)/2\rfloor}\left\lfloor\frac{L-1}{q}\right\rfloor=O(L\log L).$$
That is the main reason the code is efficient even though it evaluates \(d(n)\) for every \(n\) below the limit.
After the sieve, each candidate \(a \lt L\) produces a partner \(b=d(a)\). To decide whether \(a\) is amicable, the algorithm must still verify that \(d(b)=a\). If \(b \lt L\), the answer is already stored in the sieve table. If \(b\ge L\), the mathematical test is unchanged, but the table no longer reaches that far.
The implementations therefore compute \(d(b)\) on demand from the prime factorization of \(b\). If
$$b=\prod_{i=1}^{r} p_i^{e_i},$$
then multiplicativity gives
$$\sigma(b)=\prod_{i=1}^{r}\left(1+p_i+\cdots+p_i^{e_i}\right)=\prod_{i=1}^{r}\frac{p_i^{e_i+1}-1}{p_i-1},$$
so
$$d(b)=\sigma(b)-b.$$
This hybrid design is the real mathematical core of the code: precompute everything that is cheap and shared, then fall back to factorization only when the return check leaves the sieve range.
For each \(a \lt L\), the algorithm accepts \(a\) exactly when
$$d(a)=b,\qquad b\gt 0,\qquad b\ne a,\qquad d(b)=a.$$
So the scan is not looking for all numbers with unusually large or small divisor sums. It is specifically looking for nontrivial 2-cycles of the proper-divisor-sum map. If both members of a pair lie below \(L\), they will both be counted, once each, when the scan reaches them.
This also explains the checkpoint at \(L=300\): the only amicable pair below 300 is \((220,284)\), so the correct partial sum is
$$220+284=504.$$
The proper divisors of \(220\) are
$$1,2,4,5,10,11,20,22,44,55,110,$$
hence
$$d(220)=1+2+4+5+10+11+20+22+44+55+110=284.$$
The proper divisors of \(284\) are
$$1,2,4,71,142,$$
so
$$d(284)=1+2+4+71+142=220.$$
The same result appears from the factorization formula. Since \(220=2^2\cdot 5\cdot 11\),
$$\sigma(220)=(1+2+2^2)(1+5)(1+11)=7\cdot 6\cdot 12=504,$$
and therefore \(d(220)=504-220=284\). The implementations use exactly these identities, just at scale.
The C++, Python, and Java implementations first allocate an array of length \(L\) filled with zeros. They then run the divisor sieve so that each entry for \(n \lt L\) becomes the exact value of \(d(n)\).
The implementations iterate through \(a=2,3,\dots,L-1\), read \(b=d(a)\), discard fixed points with \(b=a\), and then test whether the map returns from \(b\) to \(a\). When \(b \lt L\), that second value comes straight from the table. When \(b\ge L\), the code computes it immediately from the prime factorization of \(b\) by separating the factor 2 and then checking odd divisors up to \(\sqrt{b}\).
All three implementations include the same lightweight checkpoints before producing the final answer: they verify \(d(220)=284\), \(d(284)=220\), and the partial case \(L=300\mapsto 504\). Those checks are useful because they exercise both the divisor-sum logic and the amicable-pair scan.
The sieve phase uses \(O(L)\) space and \(O(L\log L)\) time. The outer scan over candidates is linear in \(L\). Whenever an out-of-range partner \(b\) appears, the factorization fallback costs \(O(\sqrt{b})\) time in the worst case, because the implementations test divisibility by 2 and then by odd integers up to the square root.
A faithful upper bound for the full algorithm is therefore
$$O\!\left(L\log L+\sum_{\substack{2\le a\lt L\\d(a)\ge L}}\sqrt{d(a)}\right)$$
time and \(O(L)\) space. In the Project Euler range the sieve clearly dominates in practice, which is why this method is much faster than factoring every number below the limit independently.
Sei \(d(n)\) die Summe der echten Teiler von \(n\), also aller positiven Teiler, die strikt kleiner als \(n\) sind. Zwei verschiedene positive ganze Zahlen \(a\) und \(b\) bilden ein gütliches Paar, wenn
$$d(a)=b,\qquad d(b)=a,\qquad a \ne b.$$
Im Originalproblem sollen alle gütlichen Zahlen unterhalb von \(10000\) addiert werden. Die Implementierungen sind allgemeiner formuliert: Für ein beliebiges Limit \(L\) wird jedes \(a \lt L\) aufsummiert, das zu einem gütlichen Paar gehört. Der Partner \(b=d(a)\) darf also durchaus größer als \(L\) sein; unterhalb der Grenze liegen muss nur \(a\).
Das kleinste Beispiel ist \((220,284)\). Genau an diesem Beispiel sieht man bereits die Rückkehr nach zwei Anwendungen der Funktion \(d\), auf die der ganze Algorithmus testet.
Die Lösung verwendet zwei zueinander passende Beschreibungen derselben Größe \(d(n)\). Ein Sieb berechnet alle echten Teilersummen unterhalb des Suchlimits auf einmal, und eine Primfaktorzerlegung übernimmt die selteneren Fälle, in denen der Partnerwert außerhalb des vorberechneten Bereichs liegt.
Per Definition gilt
$$d(n)=\sum_{\substack{m\mid n\\1\le m\lt n}} m=\sigma(n)-n,$$
wobei \(\sigma(n)\) die Summe aller positiven Teiler von \(n\) bezeichnet. Die Bedingung für ein gütliches Paar besagt also genau, dass \(a\) und \(b\) unter der Abbildung \(d\) einen 2-Zyklus bilden. Die Zusatzbedingung \(a\ne b\) schließt vollkommene Zahlen wie \(6\) und \(28\) aus.
Man könnte jede Zahl einzeln faktorisieren, aber das würde die gemeinsame Struktur der Teilersummen ungenutzt lassen. Stattdessen legen die Implementierungen ein Feld für alle Werte \(0,1,\dots,L-1\) an und addieren jeden möglichen echten Teiler zu allen seinen Vielfachen:
$$d(kq)\mathrel{+}=q \qquad \left(q=1,2,\dots,\left\lfloor\frac{L-1}{2}\right\rfloor,\ k\ge 2,\ kq\lt L\right).$$
Die entscheidende Invariante lautet: Nachdem der Divisor \(q\) verarbeitet wurde, hat jede Zahl unterhalb von \(L\) bereits alle echten Teiler bis einschließlich \(q\) genau einmal erhalten. Weil die innere Schleife erst bei \(2q\) beginnt, trägt keine Zahl jemals zu ihrer eigenen echten Teilersumme bei.
Der Aufwand wird durch die harmonische Reihe bestimmt:
$$\sum_{q=1}^{\lfloor(L-1)/2\rfloor}\left\lfloor\frac{L-1}{q}\right\rfloor=O(L\log L).$$
Dadurch wird das Verfahren deutlich schneller, als für jedes \(n\) unterhalb des Limits alle Teiler separat zu bestimmen.
Nach dem Sieb liefert jeder Kandidat \(a \lt L\) einen Partner \(b=d(a)\). Um zu entscheiden, ob \(a\) wirklich gütlich ist, muss zusätzlich \(d(b)=a\) geprüft werden. Falls \(b \lt L\), steht dieser Wert bereits im Siebfeld. Falls \(b\ge L\), bleibt die mathematische Bedingung dieselbe, aber das vorberechnete Feld reicht nicht mehr aus.
Deshalb berechnen die Implementierungen \(d(b)\) bei Bedarf aus der Primfaktorzerlegung von \(b\). Ist
$$b=\prod_{i=1}^{r} p_i^{e_i},$$
dann folgt aus der Multiplikativität
$$\sigma(b)=\prod_{i=1}^{r}\left(1+p_i+\cdots+p_i^{e_i}\right)=\prod_{i=1}^{r}\frac{p_i^{e_i+1}-1}{p_i-1},$$
also
$$d(b)=\sigma(b)-b.$$
Genau diese Kombination macht den Code problemgerecht: Alles Billige und Gemeinsame wird vorab berechnet, und nur wenn die Rückprüfung das Limit überschreitet, wird faktorisiert.
Für jedes \(a \lt L\) akzeptiert der Algorithmus \(a\) genau dann, wenn
$$d(a)=b,\qquad b\gt 0,\qquad b\ne a,\qquad d(b)=a.$$
Gesucht sind also nicht einfach Zahlen mit besonders vielen Teilern oder mit großer Teilersumme, sondern genau die nichttrivialen 2-Zyklen der echten Teilersumme. Liegen beide Mitglieder eines Paares unterhalb von \(L\), werden im linearen Durchlauf automatisch beide mitgezählt.
Darum ist auch der Kontrollwert für \(L=300\) so klar: Unter 300 gibt es nur das Paar \((220,284)\), also ist die Teilsumme
$$220+284=504.$$
Die echten Teiler von \(220\) sind
$$1,2,4,5,10,11,20,22,44,55,110,$$
daher
$$d(220)=1+2+4+5+10+11+20+22+44+55+110=284.$$
Die echten Teiler von \(284\) sind
$$1,2,4,71,142,$$
also
$$d(284)=1+2+4+71+142=220.$$
Dasselbe Resultat sieht man auch über die Faktorisierung. Aus \(220=2^2\cdot 5\cdot 11\) folgt
$$\sigma(220)=(1+2+2^2)(1+5)(1+11)=7\cdot 6\cdot 12=504,$$
und damit \(d(220)=504-220=284\). Genau auf diese reziproke Beziehung prüfen die Implementierungen.
Die C++-, Python- und Java-Implementierungen erzeugen zuerst ein Feld der Länge \(L\), das mit Nullen gefüllt ist. Anschließend führen sie das beschriebene Teilersieb aus, sodass nach dieser Phase jeder Eintrag für \(n \lt L\) exakt den Wert \(d(n)\) enthält.
Danach läuft der Code über \(a=2,3,\dots,L-1\), liest \(b=d(a)\), verwirft Fixpunkte mit \(b=a\) und prüft dann, ob die Abbildung von \(b\) wieder nach \(a\) zurückkehrt. Für \(b \lt L\) kommt dieser Rückgabewert direkt aus dem vorberechneten Feld. Für \(b\ge L\) wird er unmittelbar aus der Primfaktorzerlegung von \(b\) bestimmt, wobei zuerst der Faktor 2 und anschließend ungerade Teiler bis \(\sqrt{b}\) getestet werden.
Alle drei Implementierungen enthalten vor der Endausgabe dieselben kleinen Kontrolltests: geprüft werden \(d(220)=284\), \(d(284)=220\) sowie der Teilfall \(L=300\mapsto 504\). Diese Tests sind nützlich, weil sie sowohl die Teilersummenberechnung als auch die Suche nach gütlichen Paaren abdecken.
Die Siebphase benötigt \(O(L)\) Speicher und \(O(L\log L)\) Zeit. Der anschließende lineare Durchlauf über die Kandidaten ist ebenfalls günstig. Immer wenn ein Partner \(b\) außerhalb des vorberechneten Bereichs liegt, kostet die Faktorzerlegung im schlechtesten Fall \(O(\sqrt{b})\) Zeit, weil die Implementierungen erst durch 2 und dann durch ungerade Zahlen bis zur Quadratwurzel testen.
Eine zum tatsächlichen Code passende obere Schranke lautet daher
$$O\!\left(L\log L+\sum_{\substack{2\le a\lt L\\d(a)\ge L}}\sqrt{d(a)}\right)$$
an Zeit und \(O(L)\) an Speicher. Im Project-Euler-Bereich dominiert praktisch klar das Sieb, weshalb dieses Verfahren viel schneller ist, als jede Zahl unterhalb des Limits einzeln zu faktorisieren.
\(d(n)\), \(n\) sayısının kendisinden küçük tüm pozitif bölenlerinin toplamı olsun. İki farklı pozitif tam sayı \(a\) ve \(b\), şu koşulu sağlıyorsa dost sayı çifti oluşturur:
$$d(a)=b,\qquad d(b)=a,\qquad a \ne b.$$
Asıl problem, \(10000\)'den küçük tüm dost sayıların toplamını ister. Uygulamalar bunu genel bir \(L\) limiti için çözer: \(a \lt L\) olup bir dost çifte ait olan her sayı toplanır. Eş sayı \(b=d(a)\) limitin altında da olabilir, üstünde de olabilir; önemli olan yalnızca \(a\)'nın \(L\)'den küçük olmasıdır.
En küçük örnek çift \((220,284)\)'tür. Bu çift, algoritmanın aradığı yapıyı açık biçimde gösterir: \(d\) fonksiyonu bir sayıyı diğerine, diğerini de tekrar ilkine götürür.
Çözüm, aynı büyüklük olan \(d(n)\) için iki farklı bakışı birleştirir. Bir elek yöntemi limit altındaki tüm uygun bölen toplamlarını tek seferde üretir; eş sayı önceden hesaplanan aralığın dışına taşarsa asal çarpanlara ayırma formülü devreye girer.
Tanım gereği
$$d(n)=\sum_{\substack{m\mid n\\1\le m\lt n}} m=\sigma(n)-n,$$
burada \(\sigma(n)\), \(n\)'in tüm pozitif bölenlerinin toplamıdır. Dolayısıyla dost sayı koşulu, \(a\) ile \(b\)'nin \(d\) dönüşümü altında bir 2-döngü oluşturmasıdır. \(a\ne b\) şartı ise \(6\) ve \(28\) gibi mükemmel sayıları dışarıda bırakır.
Her sayıyı tek tek çarpanlarına ayırmak mümkündür, ama bölen toplamlarının ortak yapısını boşa harcar. Bunun yerine uygulamalar \(0,1,\dots,L-1\) için bir dizi ayırır ve her olası uygun böleni kendi katlarına ekler:
$$d(kq)\mathrel{+}=q \qquad \left(q=1,2,\dots,\left\lfloor\frac{L-1}{2}\right\rfloor,\ k\ge 2,\ kq\lt L\right).$$
Temel değişmez şudur: Bölen \(q\) işlendiğinde, \(L\)'nin altındaki her sayı en fazla \(q\) olan tüm uygun bölenlerini tam bir kez almış olur. İç döngü \(2q\)'da başladığı için hiçbir sayı kendi uygun bölen toplamına kendisini eklemez.
Toplam iş miktarı harmonik seri tarafından belirlenir:
$$\sum_{q=1}^{\lfloor(L-1)/2\rfloor}\left\lfloor\frac{L-1}{q}\right\rfloor=O(L\log L).$$
Bu yüzden yöntem, limit altındaki her sayı için bölenleri baştan bulmaya göre çok daha verimlidir.
Elek tamamlandıktan sonra her aday \(a \lt L\) için bir eş sayı \(b=d(a)\) oluşur. \(a\)'nın gerçekten dost sayı olup olmadığını anlamak için ayrıca \(d(b)=a\) kontrol edilmelidir. Eğer \(b \lt L\) ise bu değer zaten tabloda vardır. Eğer \(b\ge L\) ise matematiksel test değişmez, fakat önceden hesaplanan tablo artık yetmez.
Bu nedenle uygulamalar \(d(b)\)'yi gerektiğinde \(b\)'nin asal çarpanlarına ayırımı üzerinden hesaplar. Eğer
$$b=\prod_{i=1}^{r} p_i^{e_i},$$
ise çarpımsallıktan
$$\sigma(b)=\prod_{i=1}^{r}\left(1+p_i+\cdots+p_i^{e_i}\right)=\prod_{i=1}^{r}\frac{p_i^{e_i+1}-1}{p_i-1},$$
dolayısıyla
$$d(b)=\sigma(b)-b$$
elde edilir. Kodun asıl matematiksel gücü bu hibrit yaklaşımda yatıyor: paylaşılan ve ucuz olan her şey önceden hesaplanıyor, yalnızca gerekli olduğunda tekil bir çarpanlara ayırma yapılıyor.
Algoritma, her \(a \lt L\) için şu durumda kabul kararı verir:
$$d(a)=b,\qquad b\gt 0,\qquad b\ne a,\qquad d(b)=a.$$
Yani amaç, yalnızca büyük bölen toplamlı sayıları toplamak değildir. Aranan nesneler, uygun bölen toplamı fonksiyonunun sabit nokta olmayan 2-döngüleridir. Bir çiftin iki elemanı da limitin altındaysa, doğrusal tarama sırasında ikisi de kendi sırası geldiğinde toplamaya katılır.
Bu yüzden \(L=300\) kontrolü anlamlıdır: 300'ün altında yalnızca \((220,284)\) çifti vardır ve kısmi toplam
$$220+284=504$$
olmalıdır.
\(220\)'nin uygun bölenleri
$$1,2,4,5,10,11,20,22,44,55,110,$$
olduğu için
$$d(220)=1+2+4+5+10+11+20+22+44+55+110=284.$$
\(284\)'ün uygun bölenleri ise
$$1,2,4,71,142,$$
ve dolayısıyla
$$d(284)=1+2+4+71+142=220.$$
Aynı sonuç çarpan formülünden de çıkar. \(220=2^2\cdot 5\cdot 11\) olduğundan
$$\sigma(220)=(1+2+2^2)(1+5)(1+11)=7\cdot 6\cdot 12=504,$$
buradan da \(d(220)=504-220=284\) gelir. Uygulamalar tam olarak bu karşılıklı ilişkiyi arar.
C++, Python ve Java uygulamaları önce uzunluğu \(L\) olan ve sıfırlarla dolu bir dizi oluşturur. Ardından bölen eleğini çalıştırarak \(n \lt L\) için her hücreyi tam olarak \(d(n)\) değerine dönüştürür.
Daha sonra kod \(a=2,3,\dots,L-1\) üzerinden gider, \(b=d(a)\) değerini okur, \(b=a\) olan sabit noktaları eler ve ardından dönüşün \(b\)'den tekrar \(a\)'ya gelip gelmediğini kontrol eder. \(b \lt L\) ise ikinci değer doğrudan tablodan alınır. \(b\ge L\) ise önce 2 çarpanı ayrılır, sonra \(\sqrt{b}\)'ye kadar tek bölenler denenerek asal çarpanlara ayırma üzerinden hesap yapılır.
Üç uygulama da son cevabı üretmeden önce aynı küçük kontrolleri çalıştırır: \(d(220)=284\), \(d(284)=220\) ve \(L=300\mapsto 504\). Bu kontroller hem bölen toplamı mantığını hem de dost çift taramasını aynı anda sınar.
Elek aşaması \(O(L)\) bellek ve \(O(L\log L)\) zaman kullanır. Adaylar üzerinde yapılan ikinci geçiş doğrusaldır. Limit dışına taşan bir eş sayı \(b\) çıktığında, geri dönüş kontrolü en kötü durumda \(O(\sqrt{b})\) zaman alır; çünkü uygulamalar önce 2'yi, ardından karekök sınırına kadar tek bölenleri dener.
Dolayısıyla gerçek koda sadık bir üst sınır
$$O\!\left(L\log L+\sum_{\substack{2\le a\lt L\\d(a)\ge L}}\sqrt{d(a)}\right)$$
zamandır ve bellek karmaşıklığı \(O(L)\)'dir. Project Euler aralığında pratikte baskın terim açıkça elek kısmıdır; bu nedenle yöntem, limit altındaki her sayıyı bağımsız biçimde çarpanlarına ayırmaktan çok daha hızlıdır.
Sea \(d(n)\) la suma de los divisores propios de \(n\), es decir, de todos los divisores positivos estrictamente menores que \(n\). Dos enteros positivos distintos \(a\) y \(b\) forman un par amigable cuando
$$d(a)=b,\qquad d(b)=a,\qquad a \ne b.$$
El problema original pide sumar todos los números amigables menores que \(10000\). Las implementaciones están escritas para un límite general \(L\): se suma cada \(a \lt L\) que pertenezca a un par amigable. El compañero \(b=d(a)\) puede quedar por debajo o por encima del límite; lo único obligatorio es que \(a\) esté por debajo de \(L\).
El ejemplo más pequeño es \((220,284)\). Ese caso ya contiene la estructura exacta que debe reconocer el algoritmo: dos aplicaciones de \(d\) devuelven al punto de partida sin caer en un punto fijo.
La solución combina dos descripciones complementarias de la misma función \(d(n)\). Un tamiz calcula de una vez todas las sumas de divisores propios por debajo del límite, y una fórmula basada en la factorización prima se usa cuando el compañero queda fuera del rango precalculado.
Por definición,
$$d(n)=\sum_{\substack{m\mid n\\1\le m\lt n}} m=\sigma(n)-n,$$
donde \(\sigma(n)\) es la suma de todos los divisores positivos de \(n\). Así, la condición de amistad dice exactamente que \(a\) y \(b\) forman un ciclo de longitud 2 bajo la aplicación \(d\). La desigualdad \(a\ne b\) elimina los números perfectos como \(6\) y \(28\).
Se podría factorizar cada número por separado, pero eso desaprovecharía la estructura compartida de las sumas de divisores. En cambio, las implementaciones reservan un arreglo para \(0,1,\dots,L-1\) y añaden cada posible divisor propio a todos sus múltiplos:
$$d(kq)\mathrel{+}=q \qquad \left(q=1,2,\dots,\left\lfloor\frac{L-1}{2}\right\rfloor,\ k\ge 2,\ kq\lt L\right).$$
La invariante clave es que, después de procesar el divisor \(q\), cada número menor que \(L\) ya ha recibido todos sus divisores propios menores o iguales que \(q\), y cada uno de ellos exactamente una vez. Como el bucle interior empieza en \(2q\), ningún número se suma a sí mismo.
El costo total está controlado por la serie armónica:
$$\sum_{q=1}^{\lfloor(L-1)/2\rfloor}\left\lfloor\frac{L-1}{q}\right\rfloor=O(L\log L).$$
Esa es la razón principal por la que el método es eficiente aunque evalúe \(d(n)\) para todos los \(n\) por debajo del límite.
Una vez construido el tamiz, cada candidato \(a \lt L\) produce un compañero \(b=d(a)\). Para decidir si \(a\) es amigable, todavía hace falta comprobar que \(d(b)=a\). Si \(b \lt L\), ese valor ya está en el arreglo. Si \(b\ge L\), la condición matemática no cambia, pero el tamiz ya no llega hasta allí.
Por eso las implementaciones calculan \(d(b)\) bajo demanda a partir de la factorización prima de \(b\). Si
$$b=\prod_{i=1}^{r} p_i^{e_i},$$
entonces la multiplicatividad da
$$\sigma(b)=\prod_{i=1}^{r}\left(1+p_i+\cdots+p_i^{e_i}\right)=\prod_{i=1}^{r}\frac{p_i^{e_i+1}-1}{p_i-1},$$
y por tanto
$$d(b)=\sigma(b)-b.$$
Ese diseño híbrido es exactamente lo que hace específico a este código: precomputar todo lo compartido y barato, y dejar la factorización solo para las verificaciones de retorno que salen del rango.
Para cada \(a \lt L\), el algoritmo acepta \(a\) exactamente cuando
$$d(a)=b,\qquad b\gt 0,\qquad b\ne a,\qquad d(b)=a.$$
No se están buscando simplemente números con muchos divisores ni valores grandes de la suma de divisores. Se buscan los 2-ciclos no triviales de la función de suma de divisores propios. Si ambos miembros del par están por debajo de \(L\), ambos se contarán cuando el recorrido lineal llegue a cada uno de ellos.
Eso explica también el punto de control con \(L=300\): por debajo de 300 solo aparece el par \((220,284)\), así que la suma parcial correcta es
$$220+284=504.$$
Los divisores propios de \(220\) son
$$1,2,4,5,10,11,20,22,44,55,110,$$
de modo que
$$d(220)=1+2+4+5+10+11+20+22+44+55+110=284.$$
Los divisores propios de \(284\) son
$$1,2,4,71,142,$$
y entonces
$$d(284)=1+2+4+71+142=220.$$
La misma conclusión sale de la factorización. Como \(220=2^2\cdot 5\cdot 11\),
$$\sigma(220)=(1+2+2^2)(1+5)(1+11)=7\cdot 6\cdot 12=504,$$
por lo tanto \(d(220)=504-220=284\). Las implementaciones no hacen otra cosa que repetir este criterio de forma sistemática.
Las implementaciones en C++, Python y Java reservan primero un arreglo de longitud \(L\) lleno de ceros. Luego ejecutan el tamiz de divisores, de modo que cada posición correspondiente a \(n \lt L\) termina conteniendo exactamente \(d(n)\).
Después el código recorre \(a=2,3,\dots,L-1\), lee \(b=d(a)\), descarta los puntos fijos con \(b=a\) y comprueba si desde \(b\) se vuelve a \(a\). Si \(b \lt L\), el valor de retorno sale directamente del arreglo. Si \(b\ge L\), se calcula en ese momento a partir de la factorización prima de \(b\), separando primero el factor 2 y luego probando divisores impares hasta \(\sqrt{b}\).
Las tres implementaciones incluyen las mismas verificaciones pequeñas antes de producir la respuesta final: confirman \(d(220)=284\), \(d(284)=220\) y el caso parcial \(L=300\mapsto 504\). Esas comprobaciones son útiles porque validan tanto la lógica del tamiz como la detección de pares amigables.
La fase del tamiz usa \(O(L)\) espacio y \(O(L\log L)\) tiempo. El recorrido posterior sobre los candidatos es lineal. Cuando aparece un compañero fuera del rango precalculado, la factorización de respaldo cuesta \(O(\sqrt{b})\) tiempo en el peor caso, porque las implementaciones prueban primero con 2 y luego con impares hasta la raíz cuadrada.
Un cota superior fiel al código es por tanto
$$O\!\left(L\log L+\sum_{\substack{2\le a\lt L\\d(a)\ge L}}\sqrt{d(a)}\right)$$
de tiempo y \(O(L)\) de espacio. En el rango de Project Euler domina claramente el tamiz, y por eso el método es mucho más rápido que factorizar por separado todos los números menores que el límite.
设 \(d(n)\) 表示 \(n\) 的真因数和,也就是所有严格小于 \(n\) 的正因数之和。如果两个不同的正整数 \(a\) 和 \(b\) 满足
$$d(a)=b,\qquad d(b)=a,\qquad a \ne b,$$
那么它们就是一对友爱数。原题要求把所有小于 \(10000\) 的友爱数相加。这里的实现把问题写成一般的上界 \(L\):把所有满足 \(a \lt L\) 且属于某个友爱数对的 \(a\) 全部加起来。注意配对值 \(b=d(a)\) 可以小于 \(L\),也可以大于等于 \(L\);题目真正限制的是被计入答案的那个数 \(a\),不是整个数对都必须落在范围内。
最小的经典例子是 \((220,284)\)。这个例子已经把题目的本质完全展示出来了:真因数和函数 \(d\) 把 220 送到 284,再把 284 送回 220。
这道题的实现并不是只靠一种公式。它同时使用了两种互补的数学视角:一方面用筛法批量计算区间内全部的真因数和,另一方面在需要时用素因数分解公式补算区间外的配对值。真正的效率正来自这两部分的配合。
按定义有
$$d(n)=\sum_{\substack{m\mid n\\1\le m\lt n}} m=\sigma(n)-n,$$
其中 \(\sigma(n)\) 表示 \(n\) 的全部正因数之和。所以“\(a\) 与 \(b\) 互为友爱数”可以理解成:在映射 \(d\) 之下,\(a\) 和 \(b\) 构成一个长度为 2 的循环。额外的条件 \(a\ne b\) 很重要,因为它把 \(6\)、\(28\) 这类满足 \(d(n)=n\) 的完全数排除掉了。
如果对每个整数都独立做试除分解,也能得到 \(d(n)\),但那样没有利用到“许多数字共享同一个因数”的结构。实现采用的办法更像埃拉托斯特尼筛,不过它不是标记素数,而是累计真因数和。对每个可能的真因数 \(q\),把它加到所有小于 \(L\) 的倍数上:
$$d(kq)\mathrel{+}=q \qquad \left(q=1,2,\dots,\left\lfloor\frac{L-1}{2}\right\rfloor,\ k\ge 2,\ kq\lt L\right).$$
这个过程的关键不变量是:当外层循环处理完某个 \(q\) 以后,所有小于 \(L\) 的整数都已经收到了不超过 \(q\) 的全部真因数,而且每个真因数只被加入了一次。内层循环从 \(2q\) 开始,所以一个数绝不会把自己加进自己的真因数和里。
总操作次数由调和级数控制:
$$\sum_{q=1}^{\lfloor(L-1)/2\rfloor}\left\lfloor\frac{L-1}{q}\right\rfloor=O(L\log L).$$
也就是说,虽然代码确实为区间内每个 \(n\) 都求了 \(d(n)\),但它是用共享更新的方式完成的,而不是把每个数都从头分解一遍。
筛表建立完以后,对每个候选 \(a \lt L\) 都能立刻读出 \(b=d(a)\)。但要判断 \(a\) 是否真正属于友爱数对,还必须继续验证 \(d(b)=a\)。如果 \(b \lt L\),那么第二个值已经在筛表里;如果 \(b\ge L\),筛表就覆盖不到那里了,可数学上的判定仍然不能省略,因为题目并没有要求“配对值也必须小于 \(L\)”。
这就是为什么实现里还有第二条计算路径:当 \(b\) 超出表的范围时,就把 \(b\) 做素因数分解。若
$$b=\prod_{i=1}^{r} p_i^{e_i},$$
那么由除数和函数的乘法性可得
$$\sigma(b)=\prod_{i=1}^{r}\left(1+p_i+\cdots+p_i^{e_i}\right)=\prod_{i=1}^{r}\frac{p_i^{e_i+1}-1}{p_i-1},$$
于是
$$d(b)=\sigma(b)-b.$$
这一步不是可有可无的补丁,而是题目逻辑的一部分。实现真正做的是:凡是可以共享、可以预处理的部分都交给筛法;只有当验证路径跳出预处理范围时,才单独分解那个 \(b\)。
对每个 \(a \lt L\),算法接受它当且仅当
$$d(a)=b,\qquad b\gt 0,\qquad b\ne a,\qquad d(b)=a.$$
所以它并不是在寻找“因数和很大”的数,也不是在寻找某种单调性质,而是在寻找真因数和映射里的非平凡 2-循环。如果一对友爱数的两个成员都落在 \(L\) 以下,那么线性扫描走到它们各自的时候,二者都会被加进答案。
这也解释了实现中的小检查为什么是 \(L=300\)。因为 300 以下唯一的友爱数对就是 \((220,284)\),所以部分答案必须是
$$220+284=504.$$
\(220\) 的真因数为
$$1,2,4,5,10,11,20,22,44,55,110,$$
因此
$$d(220)=1+2+4+5+10+11+20+22+44+55+110=284.$$
\(284\) 的真因数为
$$1,2,4,71,142,$$
所以
$$d(284)=1+2+4+71+142=220.$$
从素因数分解公式也能得到同样的结论。因为 \(220=2^2\cdot 5\cdot 11\),所以
$$\sigma(220)=(1+2+2^2)(1+5)(1+11)=7\cdot 6\cdot 12=504,$$
进而 \(d(220)=504-220=284\)。实现不过是把这一判定标准推广到了整个搜索区间。
C++、Python 和 Java 三个实现都会先分配一个长度为 \(L\) 的数组,并把初值设为 0。随后运行上面的除数筛,使得所有满足 \(n \lt L\) 的位置最终都精确保存 \(d(n)\)。
接下来程序遍历 \(a=2,3,\dots,L-1\),读出 \(b=d(a)\),跳过 \(b=a\) 这种完全数情形,然后检查从 \(b\) 出发是否能回到 \(a\)。如果 \(b \lt L\),这个返回值直接从数组读取;如果 \(b\ge L\),代码就立即对 \(b\) 做因数分解,先单独处理因子 2,再测试直到 \(\sqrt{b}\) 的奇因子。
三个版本在输出最终答案前都包含相同的轻量检查:确认 \(d(220)=284\)、\(d(284)=220\),以及 \(L=300\mapsto 504\)。这些检查虽然很小,但同时覆盖了筛法路径和区间外回查路径。
筛法阶段使用 \(O(L)\) 空间和 \(O(L\log L)\) 时间。随后对所有候选 \(a\) 的扫描是线性的。若某个配对值 \(b\) 超出预处理范围,那么备用的因数分解在最坏情况下需要 \(O(\sqrt{b})\) 时间,因为实现会先试 2,再试到平方根为止的奇数因子。
因此,与实际代码一致的时间上界可以写成
$$O\!\left(L\log L+\sum_{\substack{2\le a\lt L\\d(a)\ge L}}\sqrt{d(a)}\right),$$
空间复杂度为 \(O(L)\)。在 Project Euler 的范围内,实际运行时间显然主要由筛法主导,这也是它远快于“把区间内每个数都单独分解一遍”的原因。
Пусть \(d(n)\) обозначает сумму собственных делителей числа \(n\), то есть всех положительных делителей, строго меньших самого \(n\). Два различных положительных целых числа \(a\) и \(b\) образуют дружественную пару, если
$$d(a)=b,\qquad d(b)=a,\qquad a \ne b.$$
В исходной задаче нужно сложить все дружественные числа, меньшие \(10000\). Реализации написаны для общего предела \(L\): они суммируют каждое \(a \lt L\), которое принадлежит дружественной паре. Партнер \(b=d(a)\) может быть как меньше, так и больше предела; ограничение относится именно к числу \(a\), которое попадает в ответ.
Наименьшая классическая пара — \((220,284)\). Уже на ней видно точное свойство, которое ищет алгоритм: два применения функции \(d\) возвращают нас к исходной точке, но при этом не возникает фиксированной точки.
Решение опирается на два согласованных описания одной и той же величины \(d(n)\). Сито заранее вычисляет все суммы собственных делителей внутри диапазона поиска, а формула через простые множители используется тогда, когда партнер оказывается вне этого диапазона.
По определению
$$d(n)=\sum_{\substack{m\mid n\\1\le m\lt n}} m=\sigma(n)-n,$$
где \(\sigma(n)\) — сумма всех положительных делителей числа \(n\). Поэтому условие дружественности означает, что \(a\) и \(b\) образуют цикл длины 2 относительно отображения \(d\). Дополнительное требование \(a\ne b\) отсекает совершенные числа, например \(6\) и \(28\).
Можно было бы раскладывать каждое число на множители отдельно, но тогда терялась бы общая структура задачи. Вместо этого реализации заводят массив для значений \(0,1,\dots,L-1\) и прибавляют каждый возможный собственный делитель ко всем его кратным:
$$d(kq)\mathrel{+}=q \qquad \left(q=1,2,\dots,\left\lfloor\frac{L-1}{2}\right\rfloor,\ k\ge 2,\ kq\lt L\right).$$
Главная инварианта такова: после обработки делителя \(q\) каждое число ниже \(L\) уже получило все свои собственные делители, не превосходящие \(q\), причем каждый ровно один раз. Поскольку внутренний цикл начинается с \(2q\), число никогда не прибавляет само себя к собственной сумме делителей.
Полная стоимость этой фазы определяется гармоническим рядом:
$$\sum_{q=1}^{\lfloor(L-1)/2\rfloor}\left\lfloor\frac{L-1}{q}\right\rfloor=O(L\log L).$$
Именно поэтому сито дает выигрыш по сравнению с независимым вычислением делителей для каждого числа по отдельности.
После построения сита каждый кандидат \(a \lt L\) сразу дает значение \(b=d(a)\). Но чтобы понять, является ли \(a\) дружественным числом, нужно еще проверить равенство \(d(b)=a\). Если \(b \lt L\), нужное значение уже есть в массиве. Если \(b\ge L\), сама математическая проверка не меняется, но заранее вычисленной таблицы уже недостаточно.
Поэтому реализации вычисляют \(d(b)\) по требованию, используя разложение \(b\) на простые множители. Если
$$b=\prod_{i=1}^{r} p_i^{e_i},$$
то из мультипликативности следует
$$\sigma(b)=\prod_{i=1}^{r}\left(1+p_i+\cdots+p_i^{e_i}\right)=\prod_{i=1}^{r}\frac{p_i^{e_i+1}-1}{p_i-1},$$
а значит,
$$d(b)=\sigma(b)-b.$$
Именно эта гибридная схема соответствует коду: все общие и дешевые вычисления делаются заранее, а отдельная факторизация вызывается только тогда, когда обратная проверка выходит за пределы сита.
Для каждого \(a \lt L\) алгоритм принимает число тогда и только тогда, когда выполнено
$$d(a)=b,\qquad b\gt 0,\qquad b\ne a,\qquad d(b)=a.$$
Иначе говоря, ищутся не просто числа с большой суммой делителей, а именно нетривиальные 2-циклы отображения суммы собственных делителей. Если оба члена пары лежат ниже \(L\), то в линейном проходе они будут включены по отдельности, когда очередь дойдет до каждого из них.
Этим объясняется и встроенная контрольная точка \(L=300\): ниже 300 существует только пара \((220,284)\), поэтому частичная сумма должна быть равна
$$220+284=504.$$
Собственные делители числа \(220\) таковы:
$$1,2,4,5,10,11,20,22,44,55,110,$$
поэтому
$$d(220)=1+2+4+5+10+11+20+22+44+55+110=284.$$
Собственные делители числа \(284\) равны
$$1,2,4,71,142,$$
и потому
$$d(284)=1+2+4+71+142=220.$$
То же самое видно и через факторизацию. Так как \(220=2^2\cdot 5\cdot 11\), имеем
$$\sigma(220)=(1+2+2^2)(1+5)(1+11)=7\cdot 6\cdot 12=504,$$
следовательно, \(d(220)=504-220=284\). Реализации в точности и проверяют существование такой взаимной связи.
Реализации на C++, Python и Java сначала выделяют массив длины \(L\), заполненный нулями. Затем они запускают описанное сито делителей, так что после этой фазы каждая позиция для \(n \lt L\) содержит точное значение \(d(n)\).
Далее код перебирает \(a=2,3,\dots,L-1\), читает \(b=d(a)\), пропускает фиксированные точки с \(b=a\) и затем проверяет, возвращает ли отображение число \(b\) обратно к \(a\). Если \(b \lt L\), нужное значение берется прямо из массива. Если \(b\ge L\), оно вычисляется немедленно по разложению \(b\) на простые множители, при этом сначала выделяется фактор 2, а затем проверяются нечетные делители до \(\sqrt{b}\).
Во всех трех реализациях перед окончательным выводом выполняются одинаковые небольшие тесты: проверяются равенства \(d(220)=284\), \(d(284)=220\) и частный случай \(L=300\mapsto 504\). Эти проверки удобны тем, что одновременно затрагивают и сито, и логику поиска дружественных пар.
Фаза сита требует \(O(L)\) памяти и \(O(L\log L)\) времени. Последующий проход по кандидатам линеен. Если появляется партнер \(b\) вне предвычисленного диапазона, запасной путь с факторизацией требует в худшем случае \(O(\sqrt{b})\) времени, поскольку реализации сначала проверяют делимость на 2, а затем перебирают нечетные числа до квадратного корня.
Поэтому корректная по отношению к коду верхняя оценка имеет вид
$$O\!\left(L\log L+\sum_{\substack{2\le a\lt L\\d(a)\ge L}}\sqrt{d(a)}\right)$$
по времени и \(O(L)\) по памяти. В диапазоне Project Euler на практике доминирует именно сито, поэтому метод оказывается намного быстрее, чем отдельная факторизация каждого числа ниже предела.
لتكن \(d(n)\) مجموع القواسم الصحيحة لـ \(n\)، أي جميع القواسم الموجبة الأصغر من \(n\) نفسه. ويكوّن العددان الموجبان المختلفان \(a\) و\(b\) زوجًا وديًا إذا تحقق
$$d(a)=b,\qquad d(b)=a,\qquad a \ne b.$$
المسألة الأصلية تطلب جمع جميع الأعداد الودية الأصغر من \(10000\). أما التطبيقات هنا فمكتوبة لحد عام \(L\): فهي تجمع كل \(a \lt L\) ينتمي إلى زوج ودي. وقد يكون العدد المرافق \(b=d(a)\) أصغر من الحد أو أكبر منه؛ الشرط الفعلي هو أن يكون \(a\) نفسه دون \(L\).
أصغر مثال معروف هو الزوج \((220,284)\). وهذا المثال يكشف الفكرة الأساسية كاملة: دالة مجموع القواسم الصحيحة ترسل 220 إلى 284 ثم تعيد 284 إلى 220.
الحل لا يعتمد على أداة واحدة فقط، بل يجمع بين وصفيْن متكاملين للقيمة نفسها \(d(n)\). هناك منخل يحسب جميع مجاميع القواسم الصحيحة داخل مجال البحث دفعة واحدة، وهناك صيغة مبنية على التحليل إلى عوامل أولية تُستخدم عندما يقع العدد المرافق خارج المجال المحسوب مسبقًا.
بحسب التعريف،
$$d(n)=\sum_{\substack{m\mid n\\1\le m\lt n}} m=\sigma(n)-n,$$
حيث تمثل \(\sigma(n)\) مجموع جميع القواسم الموجبة للعدد \(n\). لذلك فإن شرط الودية يعني بالضبط أن \(a\) و\(b\) يشكلان دورة طولها 2 تحت التطبيق \(d\). أما الشرط \(a\ne b\) فيستبعد الأعداد الكاملة مثل \(6\) و\(28\).
يمكن من حيث المبدأ تحليل كل عدد على حدة، لكن ذلك يهدر البنية المشتركة بين مجاميع القواسم. لذا تنشئ التطبيقات مصفوفة للقيم \(0,1,\dots,L-1\)، ثم تضيف كل قاسم صحيح ممكن إلى جميع مضاعفاته:
$$d(kq)\mathrel{+}=q \qquad \left(q=1,2,\dots,\left\lfloor\frac{L-1}{2}\right\rfloor,\ k\ge 2,\ kq\lt L\right).$$
الثابت الأساسي هنا هو الآتي: بعد إنهاء معالجة القاسم \(q\)، يكون كل عدد أصغر من \(L\) قد استقبل جميع قواسمه الصحيحة التي لا تتجاوز \(q\)، وكل قاسم منها أُضيف مرة واحدة فقط. وبما أن الحلقة الداخلية تبدأ من \(2q\)، فإن العدد لا يضيف نفسه إلى مجموع قواسمه الصحيحة.
ويتحكم في عدد العمليات الكلي المتسلسلة التوافقية:
$$\sum_{q=1}^{\lfloor(L-1)/2\rfloor}\left\lfloor\frac{L-1}{q}\right\rfloor=O(L\log L).$$
ولهذا السبب يكون هذا الأسلوب أسرع بكثير من إعادة استخراج القواسم لكل عدد من جديد.
بعد بناء المنخل، يعطي كل مرشح \(a \lt L\) قيمة مرافقة \(b=d(a)\). لكن للحكم على أن \(a\) ودي فعلًا لا بد من التحقق أيضًا من أن \(d(b)=a\). إذا كان \(b \lt L\)، فالقيمة المطلوبة موجودة أصلًا داخل المصفوفة. أما إذا كان \(b\ge L\)، فشرط المسألة لا يتغير، لكن الجدول المحسوب مسبقًا لم يعد يغطي هذا العدد.
لهذا السبب تحسب التطبيقات \(d(b)\) عند الحاجة انطلاقًا من التحليل الأولي للعدد \(b\). فإذا كان
$$b=\prod_{i=1}^{r} p_i^{e_i},$$
فإن خاصية الضرب تعطي
$$\sigma(b)=\prod_{i=1}^{r}\left(1+p_i+\cdots+p_i^{e_i}\right)=\prod_{i=1}^{r}\frac{p_i^{e_i+1}-1}{p_i-1},$$
ومن ثم
$$d(b)=\sigma(b)-b.$$
وهذه ليست إضافة عرضية، بل هي قلب الفكرة الحسابية في التنفيذ: كل ما يمكن مشاركته بين الأعداد يُحسب مسبقًا بالمنخل، وما يخرج عن هذا النطاق فقط يُفحص بتحليل منفصل.
يقبل الخوارزمية العدد \(a \lt L\) بالضبط عندما يتحقق
$$d(a)=b,\qquad b\gt 0,\qquad b\ne a,\qquad d(b)=a.$$
إذًا هي لا تبحث عن أعداد ذات قواسم كثيرة فحسب، ولا عن نمط أحادي بسيط، بل عن الدورات غير التافهة ذات الطول 2 لدالة مجموع القواسم الصحيحة. وإذا كان عضوا الزوج كلاهما دون \(L\)، فسيُضاف كل واحد منهما عندما يصل المسح الخطي إليه.
ولهذا كانت نقطة الفحص عند \(L=300\) مناسبة جدًا: لا يوجد تحت 300 إلا الزوج \((220,284)\)، ولذلك يجب أن يكون المجموع الجزئي
$$220+284=504.$$
القواسم الصحيحة للعدد \(220\) هي
$$1,2,4,5,10,11,20,22,44,55,110,$$
ومن ثم
$$d(220)=1+2+4+5+10+11+20+22+44+55+110=284.$$
أما القواسم الصحيحة للعدد \(284\) فهي
$$1,2,4,71,142,$$
وبالتالي
$$d(284)=1+2+4+71+142=220.$$
وتظهر النتيجة نفسها من صيغة التحليل الأولي. بما أن \(220=2^2\cdot 5\cdot 11\)، فإن
$$\sigma(220)=(1+2+2^2)(1+5)(1+11)=7\cdot 6\cdot 12=504,$$
ومن ثم \(d(220)=504-220=284\). هذا بالضبط هو النمط الذي تبحث عنه التطبيقات في كامل المجال.
تبدأ تطبيقات C++ وPython وJava بإنشاء مصفوفة طولها \(L\) ومملوءة بالأصفار. ثم تُشغّل منخل القواسم السابق بحيث تصبح كل خانة تخص \(n \lt L\) مساوية تمامًا لـ \(d(n)\).
بعد ذلك يمر التنفيذ على \(a=2,3,\dots,L-1\)، ويقرأ \(b=d(a)\)، ويتجاوز الحالات الثابتة التي فيها \(b=a\)، ثم يتحقق هل تعود الدالة من \(b\) إلى \(a\) أم لا. إذا كان \(b \lt L\) فالقيمة تُقرأ مباشرة من الجدول. وإذا كان \(b\ge L\)، تُحسب فورًا من التحليل الأولي للعدد \(b\)، مع معالجة العامل 2 أولًا ثم اختبار القواسم الفردية حتى \(\sqrt{b}\).
تحتوي التطبيقات الثلاثة على اختبارات خفيفة قبل إخراج النتيجة النهائية: التحقق من \(d(220)=284\)، ومن \(d(284)=220\)، ومن الحالة الجزئية \(L=300\mapsto 504\). وهذه الاختبارات مفيدة لأنها تمس مسار المنخل ومسار الفحص خارج المجال في وقت واحد.
تستهلك مرحلة المنخل مساحة \(O(L)\) وزمنًا \(O(L\log L)\). أما المرور اللاحق على المرشحين فهو خطي في \(L\). وعندما يظهر عدد مرافق \(b\) خارج المجال المحسوب مسبقًا، فإن مسار التحليل البديل يكلف في أسوأ الأحوال \(O(\sqrt{b})\) زمنًا، لأن التطبيقات تختبر القسمة على 2 أولًا ثم على الأعداد الفردية حتى الجذر التربيعي.
لذلك يمكن كتابة حد علوي أمين للتنفيذ الحقيقي على الصورة
$$O\!\left(L\log L+\sum_{\substack{2\le a\lt L\\d(a)\ge L}}\sqrt{d(a)}\right)$$
للزمن و\(O(L)\) للمساحة. وفي مجال Project Euler تهيمن مرحلة المنخل عمليًا بوضوح، ولهذا تكون الطريقة أسرع بكثير من تحليل كل عدد دون الحد على حدة.