Problem Summary

For each pair of distinct primes \(p \lt q\), define \(M(p,q,N)\) as the largest integer not exceeding \(N\) whose only prime divisors are exactly \(p\) and \(q\), with both primes appearing at least once. In other words, valid numbers have the form \(p^a q^b\) with \(a,b \ge 1\). The goal is to compute

$$S(N)=\sum_{p \lt q} M(p,q,N).$$

The key difficulty is that we must optimize over exponents for every prime pair, but we must do so without factoring every integer up to \(N\).

Mathematical Approach

Fix distinct primes \(p\) and \(q\). Then

$$M(p,q,N)=\max\{p^a q^b : a,b \ge 1,\ p^a q^b \le N\},$$

with the convention that \(M(p,q,N)=0\) when no such exponents exist.

Structure of the Admissible Numbers

By the Fundamental Theorem of Arithmetic, every positive integer has a unique prime factorization. Therefore a number counted for the pair \((p,q)\) must be of the form \(p^a q^b\) with no third prime factor. The smallest such number is \(pq\), so immediately:

$$pq \gt N \implies M(p,q,N)=0.$$

This simple observation is what allows both nested prime loops in the code to terminate early.

Optimizing the Exponents for a Fixed Pair

Suppose \(p^a\) is fixed. Then the largest admissible exponent of \(q\) is the greatest integer \(b\) such that

$$q^b \le \frac{N}{p^a}.$$

Equivalently,

$$b_{\max}(a)=\left\lfloor \log_q\!\left(\frac{N}{p^a}\right)\right\rfloor,$$

whenever \(p^a q \le N\). So for this fixed \(a\), the best candidate is

$$n_a = p^a q^{\,b_{\max}(a)}.$$

Hence

$$M(p,q,N)=\max_{1 \le a \le \lfloor \log_p(N/q)\rfloor} n_a.$$

The code does not actually use floating-point logarithms. Instead, it builds powers of \(p\) and \(q\) by repeated multiplication, which is exact and avoids rounding issues.

Why the Exponent Scan Is Sufficient

For a fixed value of \(p^a\), every feasible number has the form \(p^a q^b\). Since \(q \gt 1\), increasing \(b\) strictly increases the value. Therefore, once \(a\) is fixed, the best choice is always the largest admissible power of \(q\). There is no need to keep smaller \(q\)-powers for the same \(a\); only the maximum matters.

This turns a two-dimensional search over all exponent pairs into the following finite scan:

$$p,\ p^2,\ p^3,\dots,\qquad\text{and for each one, }q,\ q^2,\ q^3,\dots$$

stopping as soon as the product would exceed \(N\).

Prime-Pair Iteration

After generating all primes up to \(N\) with the Sieve of Eratosthenes, the program iterates over pairs \(p \lt q\). The condition \(pq \le N\) is enough to decide whether the pair can contribute anything at all. Thus:

$$S(N)=\sum_{\substack{p \lt q\\ pq \le N}} M(p,q,N).$$

The inner loop breaks once \(pq \gt N\), and the outer loop breaks once \(2p \gt N\), because \(q \ge 2\) for every prime. These are mathematically valid pruning rules, not merely implementation heuristics.

Worked Example: \(N = 100\)

The problem statement gives three useful checkpoints, and they explain the method well.

For \((p,q)=(2,3)\), the admissible values are

$$6,\ 12,\ 18,\ 24,\ 36,\ 48,\ 54,\ 72,\ 96,$$

so

$$M(2,3,100)=96.$$

For \((p,q)=(3,5)\), the admissible values are

$$15,\ 45,\ 75,$$

hence

$$M(3,5,100)=75.$$

For \((p,q)=(2,73)\), the smallest possible product is \(2\cdot 73=146 \gt 100\), so

$$M(2,73,100)=0.$$

Summing \(M(p,q,100)\) over all prime pairs gives the checkpoint

$$S(100)=2262.$$

Deriving the Loop Structure Used in Code

The helper routine max_for_pair(p, q, limit) follows the mathematics directly. It first loops over

$$p,\ p^2,\ p^3,\dots,\ p^a \le \frac{N}{q},$$

because if \(p^a \gt N/q\), then even \(p^a q\) is already too large. For each such power \(p^a\), it starts with \(p^a q\) and keeps multiplying by \(q\) while staying within the limit. The best value seen over the whole scan is precisely \(M(p,q,N)\).

The overflow-safe tests in code, such as value > limit / q before multiplying by \(q\), are simply integer forms of the inequality \(value \cdot q \le N\).

How the Code Works

The C++, Java, and Python solutions all follow the same three-stage structure.

First, sieve_primes(limit) computes the prime list up to \(N\). Second, max_for_pair(p, q, limit) evaluates \(M(p,q,N)\) by exponent scanning. Third, the main solver loops over all prime pairs with \(pq \le N\) and accumulates the corresponding maxima.

The implementation also keeps a seen array before adding a value to the sum. Mathematically, distinct prime pairs cannot produce the same nonzero number because prime factorization is unique, so this guard is defensive rather than essential. It does not change the result.

Complexity Analysis

Generating primes up to \(N\) by sieve costs \(O(N\log\log N)\) time and \(O(N)\) memory.

For a fixed pair \((p,q)\), the exponent scan performs at most \(O(\log_p N \cdot \log_q N)\) loop iterations in the worst case, and usually far fewer because the loops stop as soon as the product exceeds \(N\).

Therefore the total running time is

$$O\!\left(N\log\log N + \sum_{\substack{p \lt q\\ pq \le N}} \log_p N \cdot \log_q N\right),$$

with \(O(N)\) memory for the sieve and the marking array. In practice this is dramatically faster than factoring every integer from \(1\) to \(N\).

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=347
  2. Sieve of Eratosthenes: Wikipedia — Sieve of Eratosthenes
  3. Fundamental theorem of arithmetic: Wikipedia — Fundamental theorem of arithmetic
  4. Prime counting and asymptotic background: G. H. Hardy, E. M. Wright, An Introduction to the Theory of Numbers.
  5. Integer exponent bounds and logarithms: Wikipedia — Logarithm

Problemzusammenfassung

Für jedes Paar verschiedener Primzahlen \(p \lt q\) sei \(M(p,q,N)\) die größte Zahl \(\le N\), deren einzige Primteiler genau \(p\) und \(q\) sind, wobei beide mit positivem Exponenten vorkommen. Zulässige Zahlen haben also die Form \(p^a q^b\) mit \(a,b \ge 1\). Gesucht ist

$$S(N)=\sum_{p \lt q} M(p,q,N).$$

Die eigentliche Aufgabe besteht darin, für jedes Primzahlpaar die optimalen Exponenten zu finden, ohne alle Zahlen bis \(N\) einzeln zu faktorisieren.

Mathematischer Ansatz

Für feste verschiedene Primzahlen \(p\) und \(q\) gilt

$$M(p,q,N)=\max\{p^a q^b : a,b \ge 1,\ p^a q^b \le N\},$$

wobei per Konvention \(M(p,q,N)=0\) ist, falls es keine zulässigen Exponenten gibt.

Struktur der zulässigen Zahlen

Nach dem Fundamentalsatz der Arithmetik besitzt jede positive ganze Zahl genau eine Primfaktorzerlegung. Daher gehört eine für \((p,q)\) relevante Zahl notwendigerweise zur Form \(p^a q^b\) und darf keinen dritten Primfaktor enthalten. Die kleinste solche Zahl ist \(pq\). Also sofort:

$$pq \gt N \implies M(p,q,N)=0.$$

Genau diese Beobachtung rechtfertigt die frühen Abbrüche in beiden Primzahlschleifen.

Optimierung der Exponenten für ein festes Paar

Sei \(p^a\) fest vorgegeben. Dann ist der größte zulässige Exponent von \(q\) die größte ganze Zahl \(b\) mit

$$q^b \le \frac{N}{p^a}.$$

Äquivalent dazu:

$$b_{\max}(a)=\left\lfloor \log_q\!\left(\frac{N}{p^a}\right)\right\rfloor,$$

sofern \(p^a q \le N\). Für dieses feste \(a\) ist der beste Kandidat also

$$n_a = p^a q^{\,b_{\max}(a)}.$$

Damit folgt

$$M(p,q,N)=\max_{1 \le a \le \lfloor \log_p(N/q)\rfloor} n_a.$$

Der Code verwendet dafür jedoch keine Fließkomma-Logarithmen, sondern erzeugt Potenzen von \(p\) und \(q\) per wiederholter Multiplikation. Das ist exakt und vermeidet Rundungsprobleme.

Warum das Durchlaufen der Exponenten genügt

Ist \(p^a\) fest, dann hat jede zulässige Zahl die Form \(p^a q^b\). Weil \(q \gt 1\), wächst der Wert strikt mit \(b\). Für ein festes \(a\) ist also immer die größte noch erlaubte Potenz von \(q\) optimal. Kleinere \(q\)-Exponenten müssen nicht separat gespeichert werden.

Damit reduziert sich die zweidimensionale Suche über alle Exponentenpaare auf den endlichen Scan

$$p,\ p^2,\ p^3,\dots,\qquad\text{und dazu jeweils }q,\ q^2,\ q^3,\dots$$

mit Abbruch, sobald das Produkt \(N\) überschreiten würde.

Iteration über Primzahlpaare

Nach der Primzahlerzeugung mit dem Sieb des Eratosthenes iteriert das Programm über alle Paare \(p \lt q\). Die Bedingung \(pq \le N\) entscheidet bereits, ob das Paar überhaupt beitragen kann. Daher:

$$S(N)=\sum_{\substack{p \lt q\\ pq \le N}} M(p,q,N).$$

Die innere Schleife endet, sobald \(pq \gt N\), und die äußere endet, sobald \(2p \gt N\), denn für jede Primzahl gilt \(q \ge 2\). Das sind mathematisch korrekte Schranken, keine bloßen Heuristiken.

Beispielrechnung: \(N = 100\)

Die Aufgabenstellung liefert drei nützliche Kontrollwerte.

Für \((p,q)=(2,3)\) sind die zulässigen Werte

$$6,\ 12,\ 18,\ 24,\ 36,\ 48,\ 54,\ 72,\ 96,$$

also

$$M(2,3,100)=96.$$

Für \((p,q)=(3,5)\) erhält man

$$15,\ 45,\ 75,$$

und damit

$$M(3,5,100)=75.$$

Für \((p,q)=(2,73)\) ist schon das kleinste Produkt \(2\cdot 73=146 \gt 100\), also

$$M(2,73,100)=0.$$

Summiert man über alle Primzahlpaare, ergibt sich der Kontrollwert

$$S(100)=2262.$$

Ableitung der im Code verwendeten Schleifenstruktur

Die Hilfsfunktion max_for_pair(p, q, limit) bildet die Mathematik direkt ab. Zuerst werden die Potenzen \(p,\ p^2,\ p^3,\dots\) durchlaufen, solange

$$p^a \le \frac{N}{q},$$

denn bei \(p^a \gt N/q\) wäre schon \(p^a q\) zu groß. Für jede solche Potenz beginnt die innere Schleife bei \(p^a q\) und multipliziert weiter mit \(q\), solange die Schranke eingehalten wird. Das größte dabei gefundene Produkt ist genau \(M(p,q,N)\).

Die überlaufsicheren Tests im Code, etwa value > limit / q vor einer Multiplikation mit \(q\), sind nur ganzzahlige Umformungen der Bedingung \(value \cdot q \le N\).

Funktionsweise des Codes

Die Lösungen in C++, Java und Python haben dieselbe dreistufige Struktur.

Zuerst berechnet sieve_primes(limit) alle Primzahlen bis \(N\). Danach bestimmt max_for_pair(p, q, limit) per Exponentenscan den Wert \(M(p,q,N)\). Schließlich iteriert der Hauptteil über alle Paare mit \(pq \le N\) und summiert die Maxima.

Zusätzlich verwendet die Implementierung ein Array seen, bevor ein Wert zur Summe addiert wird. Wegen der eindeutigen Primfaktorzerlegung können verschiedene Primzahlpaare mathematisch keinen gleichen positiven Wert erzeugen; dieser Schutz ist also defensiv, aber harmlos.

Komplexitätsanalyse

Das Primzahlsieb benötigt \(O(N\log\log N)\) Zeit und \(O(N)\) Speicher.

Für ein festes Paar \((p,q)\) führt der Exponentenscan im Worst Case höchstens \(O(\log_p N \cdot \log_q N)\) Schleifendurchläufe aus, praktisch meist deutlich weniger, weil bei Überschreiten von \(N\) sofort abgebrochen wird.

Damit beträgt die Gesamtlaufzeit

$$O\!\left(N\log\log N + \sum_{\substack{p \lt q\\ pq \le N}} \log_p N \cdot \log_q N\right),$$

bei \(O(N)\) Speicher für Sieb und Markierungsarray. Das ist in der Praxis viel schneller als jede Zahl zwischen \(1\) und \(N\) einzeln zu faktorisieren.

Fußnoten und Quellen

  1. Problemseite: https://projecteuler.net/problem=347
  2. Sieb des Eratosthenes: Wikipedia
  3. Fundamentalsatz der Arithmetik: Wikipedia
  4. Hardy, Wright: An Introduction to the Theory of Numbers.
  5. Logarithmus und Exponentenschranken: Wikipedia

Problem Özeti

Her farklı asal çifti \(p \lt q\) için, \(M(p,q,N)\) değeri \(N\)'i aşmayan ve asal çarpanları tam olarak yalnızca \(p\) ile \(q\) olan en büyük tamsayıdır; ayrıca her iki asal da en az bir kez görünmelidir. Yani geçerli sayılar \(p^a q^b\) biçimindedir ve \(a,b \ge 1\). Amaç

$$S(N)=\sum_{p \lt q} M(p,q,N)$$

toplamını hesaplamaktır. Asıl zorluk, bunu \(1\)'den \(N\)'e kadar her sayıyı tek tek çarpanlarına ayırmadan yapabilmektir.

Matematiksel Yaklaşım

Farklı iki asal \(p\) ve \(q\) için

$$M(p,q,N)=\max\{p^a q^b : a,b \ge 1,\ p^a q^b \le N\}$$

olarak tanımlanır; eğer böyle üsler yoksa konvansiyon gereği \(M(p,q,N)=0\) alınır.

Geçerli Sayıların Yapısı

Aritmetiğin temel teoremine göre her pozitif tam sayının asal çarpanlara ayrılması tektir. Bu yüzden \((p,q)\) çifti için sayılabilecek bir sayı mutlaka \(p^a q^b\) biçiminde olmalı ve üçüncü bir asal içermemelidir. Böyle bir sayının en küçüğü \(pq\) olduğundan hemen şu çıkar:

$$pq \gt N \implies M(p,q,N)=0.$$

Koddaki iç ve dış asal döngülerinin erken kırılması tam olarak bu gözleme dayanır.

Sabit Bir Çift İçin Üsleri Optimize Etme

\(p^a\) sabit olsun. O zaman \(q\)'nun alabileceği en büyük üs,

$$q^b \le \frac{N}{p^a}$$

koşulunu sağlayan en büyük tam sayı \(b\)'dir. Eşdeğer olarak

$$b_{\max}(a)=\left\lfloor \log_q\!\left(\frac{N}{p^a}\right)\right\rfloor,$$

tabii ancak \(p^a q \le N\) ise. Bu durumda sabit \(a\) için en iyi aday

$$n_a = p^a q^{\,b_{\max}(a)}$$

olur. Dolayısıyla

$$M(p,q,N)=\max_{1 \le a \le \lfloor \log_p(N/q)\rfloor} n_a.$$

Kod bunu kayan noktalı logaritmalarla yapmaz; onun yerine \(p\) ve \(q\) kuvvetlerini tekrar tekrar çarparak üretir. Böylece hem tam sayı aritmetiği korunur hem de yuvarlama hataları oluşmaz.

Üs Taramasının Neden Yeterli Olduğu

\(p^a\) sabitken tüm uygun sayılar \(p^a q^b\) biçimindedir. \(q \gt 1\) olduğu için \(b\) arttıkça değer de kesin olarak büyür. O halde sabit bir \(a\) için yalnızca izin verilen en büyük \(q\) kuvveti önemlidir; daha küçük \(q\) üslerini ayrıca saklamaya gerek yoktur.

Böylece tüm üs çiftleri üzerindeki iki boyutlu arama şu sonlu taramaya indirgenir:

$$p,\ p^2,\ p^3,\dots,\qquad\text{ve her biri için }q,\ q^2,\ q^3,\dots$$

Ürün \(N\)'i aştığı anda döngü durdurulur.

Asal Çiftleri Üzerinde Dolaşma

Tüm asallar Eratosthenes süzgeciyle üretildikten sonra program \(p \lt q\) çiftlerini gezer. Bir çiftin katkı verip veremeyeceğini anlamak için \(pq \le N\) koşulu yeterlidir. Bu nedenle

$$S(N)=\sum_{\substack{p \lt q\\ pq \le N}} M(p,q,N).$$

İç döngü \(pq \gt N\) olduğunda kırılır. Dış döngü ise \(2p \gt N\) olduğunda sona erer; çünkü her asal için \(q \ge 2\) geçerlidir. Bunlar yalnızca pratik kestirmeler değil, doğrudan matematiksel sınırlandırmalardır.

Sayısal Örnek: \(N = 100\)

Problem metnindeki üç kontrol değeri yöntemi net biçimde gösterir.

\((p,q)=(2,3)\) için uygun sayılar

$$6,\ 12,\ 18,\ 24,\ 36,\ 48,\ 54,\ 72,\ 96,$$

olduğundan

$$M(2,3,100)=96.$$

\((p,q)=(3,5)\) için uygun değerler

$$15,\ 45,\ 75,$$

ve dolayısıyla

$$M(3,5,100)=75.$$

\((p,q)=(2,73)\) için en küçük olası çarpım bile \(2\cdot 73=146 \gt 100\) olduğundan

$$M(2,73,100)=0.$$

Tüm asal çiftleri için toplam alındığında kontrol sonucu

$$S(100)=2262$$

elde edilir.

Koddaki Döngü Yapısının Türetilmesi

max_for_pair(p, q, limit) yardımcı fonksiyonu matematiği doğrudan uygular. Önce

$$p,\ p^2,\ p^3,\dots,\ p^a \le \frac{N}{q}$$

kuvvetleri gezilir; çünkü \(p^a \gt N/q\) olduğunda artık \(p^a q\) bile sınırı aşar. Her böyle \(p^a\) için iç döngü \(p^a q\) ile başlar ve sınır aşılmadığı sürece \(q\) ile çarpmaya devam eder. Tarama sırasında görülen en büyük değer tam olarak \(M(p,q,N)\)'dir.

Koddaki value > limit / q veya pa > limit / p testleri, taşmayı önleyen ve \(value \cdot q \le N\), \(pa \cdot p \le N\) eşitsizliklerinin tam sayı biçiminde yazılmış halidir.

Kodun Çalışma Mantığı

C++, Java ve Python çözümleri aynı üç aşamalı yapıyı izler.

Önce sieve_primes(limit) ile \(N\)'e kadar tüm asallar üretilir. Ardından max_for_pair(p, q, limit) fonksiyonu üs taramasıyla \(M(p,q,N)\) değerini hesaplar. Son aşamada ana çözüm, \(pq \le N\) koşulunu sağlayan tüm asal çiftlerini dolaşıp bu maksimumları toplar.

Uygulama ayrıca bir seen dizisi tutar. Matematiksel olarak farklı asal çiftlerinin aynı pozitif sayıyı üretmesi mümkün değildir; çünkü asal çarpanlara ayırma tektir. Bu nedenle bu kontrol zorunlu değil, yalnızca savunmacı bir güvenlik katmanıdır.

Karmaşıklık Analizi

Süzgeç ile asal üretimi \(O(N\log\log N)\) zaman ve \(O(N)\) bellek gerektirir.

Sabit bir \((p,q)\) çifti için üs taraması en kötü durumda \(O(\log_p N \cdot \log_q N)\) kadar yineleme yapar; pratikte genellikle daha azdır, çünkü ürün \(N\)'i geçtiği anda döngüler sona erer.

Böylece toplam çalışma süresi

$$O\!\left(N\log\log N + \sum_{\substack{p \lt q\\ pq \le N}} \log_p N \cdot \log_q N\right)$$

şeklindedir ve bellek kullanımı süzgeç ile işaretleme dizisi nedeniyle \(O(N)\)'dir. Bu yaklaşım, \(1\) ile \(N\) arasındaki her sayıyı çarpanlarına ayırmaya kıyasla çok daha hızlıdır.

Dipnotlar ve Kaynakça

  1. Problem sayfası: https://projecteuler.net/problem=347
  2. Eratosthenes süzgeci: Wikipedia
  3. Aritmetiğin temel teoremi: Wikipedia
  4. Hardy, Wright: An Introduction to the Theory of Numbers.
  5. Logaritma: Wikipedia

Resumen del Problema

Para cada par de primos distintos \(p \lt q\), se define \(M(p,q,N)\) como el mayor entero \(\le N\) cuyos únicos divisores primos son exactamente \(p\) y \(q\), apareciendo ambos al menos una vez. Es decir, los números válidos tienen la forma \(p^a q^b\) con \(a,b \ge 1\). Se desea calcular

$$S(N)=\sum_{p \lt q} M(p,q,N).$$

La dificultad consiste en optimizar los exponentes para cada par primo sin factorizar todos los enteros hasta \(N\).

Enfoque Matemático

Fijados dos primos distintos \(p\) y \(q\),

$$M(p,q,N)=\max\{p^a q^b : a,b \ge 1,\ p^a q^b \le N\},$$

y por convenio \(M(p,q,N)=0\) si no existe ninguna pareja de exponentes válida.

Estructura de los números admisibles

Por el teorema fundamental de la aritmética, toda factorización prima es única. Por tanto, un número asociado al par \((p,q)\) debe ser exactamente de la forma \(p^a q^b\), sin un tercer primo. El menor de ellos es \(pq\), así que inmediatamente:

$$pq \gt N \implies M(p,q,N)=0.$$

Esta observación justifica los cortes tempranos en los dos bucles de primos.

Optimización de exponentes para un par fijo

Supongamos fijo \(p^a\). Entonces el mayor exponente permitido de \(q\) es el mayor entero \(b\) tal que

$$q^b \le \frac{N}{p^a}.$$

Equivalentemente,

$$b_{\max}(a)=\left\lfloor \log_q\!\left(\frac{N}{p^a}\right)\right\rfloor,$$

siempre que \(p^a q \le N\). Para ese \(a\) fijo, el mejor candidato es entonces

$$n_a = p^a q^{\,b_{\max}(a)}.$$

De ahí se obtiene

$$M(p,q,N)=\max_{1 \le a \le \lfloor \log_p(N/q)\rfloor} n_a.$$

El código evita usar logaritmos en coma flotante: construye potencias de \(p\) y \(q\) mediante multiplicaciones repetidas, lo cual es exacto y evita errores de redondeo.

Por qué basta con recorrer exponentes

Si \(p^a\) queda fijado, todos los candidatos tienen la forma \(p^a q^b\). Como \(q \gt 1\), al aumentar \(b\) el valor crece estrictamente. Por ello, para un \(a\) fijo, solo importa la mayor potencia admisible de \(q\); las potencias menores no pueden mejorar el resultado.

Así, una búsqueda bidimensional sobre todos los pares \((a,b)\) se reduce al barrido finito

$$p,\ p^2,\ p^3,\dots,\qquad\text{y para cada uno }q,\ q^2,\ q^3,\dots$$

deteniéndose cuando el producto superaría \(N\).

Iteración sobre pares primos

Una vez generados todos los primos hasta \(N\) con la criba de Eratóstenes, el programa recorre los pares \(p \lt q\). La condición \(pq \le N\) ya decide si el par puede aportar algo. Por tanto,

$$S(N)=\sum_{\substack{p \lt q\\ pq \le N}} M(p,q,N).$$

El bucle interior termina cuando \(pq \gt N\), y el exterior cuando \(2p \gt N\), porque todo primo satisface \(q \ge 2\). Son podas matemáticamente correctas.

Ejemplo trabajado: \(N = 100\)

El enunciado proporciona tres puntos de control muy útiles.

Para \((p,q)=(2,3)\), los valores admisibles son

$$6,\ 12,\ 18,\ 24,\ 36,\ 48,\ 54,\ 72,\ 96,$$

de modo que

$$M(2,3,100)=96.$$

Para \((p,q)=(3,5)\), los candidatos son

$$15,\ 45,\ 75,$$

y por tanto

$$M(3,5,100)=75.$$

Para \((p,q)=(2,73)\), el menor producto posible ya es \(2\cdot 73=146 \gt 100\), luego

$$M(2,73,100)=0.$$

Al sumar todos los pares primos se obtiene el valor de control

$$S(100)=2262.$$

Derivación de la estructura de bucles usada en el código

La rutina auxiliar max_for_pair(p, q, limit) sigue la derivación matemática casi literalmente. Primero recorre

$$p,\ p^2,\ p^3,\dots,\ p^a \le \frac{N}{q},$$

porque si \(p^a \gt N/q\), entonces incluso \(p^a q\) ya es demasiado grande. Para cada potencia \(p^a\), el bucle interior empieza en \(p^a q\) y sigue multiplicando por \(q\) mientras no se exceda el límite. El mejor valor encontrado durante todo el barrido es exactamente \(M(p,q,N)\).

Las comprobaciones del tipo value > limit / q antes de multiplicar por \(q\) son simplemente versiones enteras y seguras de la desigualdad \(value \cdot q \le N\).

Cómo funciona el código

Las versiones en C++, Java y Python comparten la misma estructura en tres etapas.

Primero, sieve_primes(limit) genera la lista de primos hasta \(N\). Después, max_for_pair(p, q, limit) calcula \(M(p,q,N)\) mediante el barrido de exponentes. Por último, la función principal recorre todos los pares con \(pq \le N\) y acumula los máximos.

Además, el programa mantiene un arreglo seen antes de sumar un valor. Matemáticamente no es necesario, porque dos pares distintos de primos no pueden producir el mismo entero positivo debido a la unicidad de la factorización prima; es solo una protección defensiva.

Complejidad

La criba de primos cuesta \(O(N\log\log N)\) en tiempo y \(O(N)\) en memoria.

Para un par fijo \((p,q)\), el barrido de exponentes realiza como máximo \(O(\log_p N \cdot \log_q N)\) iteraciones en el peor caso, y normalmente menos porque los bucles se detienen tan pronto como el producto excede \(N\).

En conjunto, el tiempo total es

$$O\!\left(N\log\log N + \sum_{\substack{p \lt q\\ pq \le N}} \log_p N \cdot \log_q N\right),$$

con \(O(N)\) memoria para la criba y el arreglo de marcas. En la práctica, esto es muy superior a factorizar todos los enteros desde \(1\) hasta \(N\).

Notas y Referencias

  1. Página del problema: https://projecteuler.net/problem=347
  2. Criba de Eratóstenes: Wikipedia
  3. Teorema fundamental de la aritmética: Wikipedia
  4. Hardy, Wright: An Introduction to the Theory of Numbers.
  5. Logaritmo: Wikipedia

问题概述

对每一对不同素数 \(p \lt q\),定义 \(M(p,q,N)\) 为不超过 \(N\) 的最大整数,并且它的素因子恰好只有 \(p\) 和 \(q\),且这两个素数都至少出现一次。也就是说,合法数一定形如 \(p^a q^b\),其中 \(a,b \ge 1\)。目标是计算

$$S(N)=\sum_{p \lt q} M(p,q,N).$$

难点在于:我们要对每一对素数优化指数,但又不能把 \(1\) 到 \(N\) 的所有整数都逐个分解质因数。

数学方法

固定不同素数 \(p,q\) 后,有

$$M(p,q,N)=\max\{p^a q^b : a,b \ge 1,\ p^a q^b \le N\},$$

如果不存在满足条件的指数对,则约定 \(M(p,q,N)=0\)。

合法数的结构

根据算术基本定理,每个正整数的素因子分解都是唯一的。因此,对应于 \((p,q)\) 的数必须正好是 \(p^a q^b\) 的形式,不能再含有第三个素因子。这样的最小数是 \(pq\),所以立刻得到

$$pq \gt N \implies M(p,q,N)=0.$$

代码中两层素数循环之所以可以提前结束,正是因为这个简单结论。

固定素数对时如何优化指数

设 \(p^a\) 已固定,则 \(q\) 的最大允许指数是满足

$$q^b \le \frac{N}{p^a}$$

的最大整数 \(b\)。等价地,

$$b_{\max}(a)=\left\lfloor \log_q\!\left(\frac{N}{p^a}\right)\right\rfloor,$$

前提是 \(p^a q \le N\)。因此,对这个固定的 \(a\),最优候选数就是

$$n_a = p^a q^{\,b_{\max}(a)}.$$

从而

$$M(p,q,N)=\max_{1 \le a \le \lfloor \log_p(N/q)\rfloor} n_a.$$

代码并没有真的去算浮点对数,而是通过反复整数乘法生成 \(p\) 和 \(q\) 的幂,这样完全精确,也避免 了舍入误差。

为什么只扫描指数就够了

当 \(p^a\) 固定时,所有合法候选都形如 \(p^a q^b\)。因为 \(q \gt 1\),随着 \(b\) 增大,数值严格增 大。所以对固定的 \(a\),只需要保留不超过 \(N\) 的最大 \(q\) 幂,较小的 \(q\) 幂不可能更优。

这就把原本对所有指数对 \((a,b)\) 的二维搜索,化成了有限扫描:

$$p,\ p^2,\ p^3,\dots,\qquad\text{并且对每个它再扫描 }q,\ q^2,\ q^3,\dots$$

一旦乘积会超过 \(N\),循环就停止。

遍历所有素数对

先用埃氏筛生成所有不超过 \(N\) 的素数,然后枚举所有满足 \(p \lt q\) 的素数对。条件 \(pq \le N\) 已经足以判断这个素数对是否可能贡献非零值,因此

$$S(N)=\sum_{\substack{p \lt q\\ pq \le N}} M(p,q,N).$$

当 \(pq \gt N\) 时,内层循环可以直接终止;当 \(2p \gt N\) 时,外层循环也可以终止,因为任意素数都 满足 \(q \ge 2\)。这些都不是经验性剪枝,而是严格的数学界限。

例子:\(N = 100\)

题目本身给出了几个很好的校验值。

对 \((p,q)=(2,3)\),合法数为

$$6,\ 12,\ 18,\ 24,\ 36,\ 48,\ 54,\ 72,\ 96,$$

所以

$$M(2,3,100)=96.$$

对 \((p,q)=(3,5)\),合法数为

$$15,\ 45,\ 75,$$

因此

$$M(3,5,100)=75.$$

对 \((p,q)=(2,73)\),最小可能乘积已经是 \(2\cdot 73=146 \gt 100\),于是

$$M(2,73,100)=0.$$

把所有素数对的结果加起来,可得到校验值

$$S(100)=2262.$$

代码中循环结构的数学来源

辅助函数 max_for_pair(p, q, limit) 基本就是上述推导的直接实现。它先枚举

$$p,\ p^2,\ p^3,\dots,\ p^a \le \frac{N}{q},$$

因为一旦 \(p^a \gt N/q\),连最小的 \(p^a q\) 都已经超界。对每个这样的 \(p^a\),内层循环从 \(p^a q\) 开始,不断乘以 \(q\),直到超过上界为止。整个过程中出现的最大值,恰好就是 \(M(p,q,N)\)。

代码里像 value > limit / q 这样的判断,是为了在整数范围内安全地表达 \(value \cdot q \le N\),从而避免溢出。

代码如何工作

C++、Java 和 Python 版本都遵循相同的三步结构。

第一步,sieve_primes(limit) 生成所有素数。第二步, max_for_pair(p, q, limit) 通过指数扫描计算 \(M(p,q,N)\)。第三步,主过程枚举所有满足 \(pq \le N\) 的素数对,并把对应最大值累加起来。

实现中还保留了一个 seen 标记数组,再把值加入总和。严格来说这在数学上不是必需的, 因为不同素数对不可能对应同一个正整数,原因正是质因数分解唯一性;这个数组只是防御性保护。

复杂度分析

筛出所有不超过 \(N\) 的素数需要 \(O(N\log\log N)\) 时间和 \(O(N)\) 空间。

对于固定的 \((p,q)\),指数扫描在最坏情况下至多进行 \(O(\log_p N \cdot \log_q N)\) 次迭代;实际通常更少,因为一旦乘积超过 \(N\) 就立刻停止。

因此总时间复杂度为

$$O\!\left(N\log\log N + \sum_{\substack{p \lt q\\ pq \le N}} \log_p N \cdot \log_q N\right),$$

空间复杂度为 \(O(N)\),主要来自筛法数组和标记数组。和把 \(1\) 到 \(N\) 每个整数都做因数分解相比, 这个方法要高效得多。

参考资料

  1. 题目页面: https://projecteuler.net/problem=347
  2. 埃拉托斯特尼筛法: Wikipedia
  3. 算术基本定理: Wikipedia
  4. Hardy, Wright: An Introduction to the Theory of Numbers.
  5. 对数: Wikipedia

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

Для каждой пары различных простых чисел \(p \lt q\) величина \(M(p,q,N)\) определяется как наибольшее число \(\le N\), чьи простые делители суть ровно \(p\) и \(q\), причём оба входят с положительной степенью. Иначе говоря, допустимые числа имеют вид \(p^a q^b\), где \(a,b \ge 1\). Требуется вычислить

$$S(N)=\sum_{p \lt q} M(p,q,N).$$

Сложность задачи в том, что нужно оптимизировать показатели для каждой пары простых, не раскладывая на множители все числа от \(1\) до \(N\).

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

Для фиксированных различных простых \(p\) и \(q\)

$$M(p,q,N)=\max\{p^a q^b : a,b \ge 1,\ p^a q^b \le N\},$$

а если допустимых степеней нет, то по соглашению \(M(p,q,N)=0\).

Структура допустимых чисел

По основной теореме арифметики разложение на простые множители единственно. Значит, число, относящееся к паре \((p,q)\), обязано иметь вид \(p^a q^b\) и не может содержать третий простой множитель. Наименьшее такое число равно \(pq\), поэтому сразу имеем

$$pq \gt N \implies M(p,q,N)=0.$$

Именно это наблюдение оправдывает ранние остановки в обеих циклах по простым.

Оптимизация степеней для фиксированной пары

Пусть степень \(p^a\) зафиксирована. Тогда максимальный допустимый показатель \(b\) для \(q\) есть наибольшее целое число, удовлетворяющее

$$q^b \le \frac{N}{p^a}.$$

Эквивалентно,

$$b_{\max}(a)=\left\lfloor \log_q\!\left(\frac{N}{p^a}\right)\right\rfloor,$$

при условии, что \(p^a q \le N\). Следовательно, для данного \(a\) лучший кандидат равен

$$n_a = p^a q^{\,b_{\max}(a)}.$$

Отсюда

$$M(p,q,N)=\max_{1 \le a \le \lfloor \log_p(N/q)\rfloor} n_a.$$

В коде плавающая арифметика не используется: степени \(p\) и \(q\) строятся последовательными умножениями, что даёт точные целочисленные значения и исключает ошибки округления.

Почему достаточно перебора степеней

Если \(p^a\) фиксировано, все допустимые числа имеют вид \(p^a q^b\). Поскольку \(q \gt 1\), значение строго возрастает с ростом \(b\). Значит, для фиксированного \(a\) достаточно рассмотреть только наибольшую разрешённую степень \(q\); меньшие степени уже не дадут максимум.

Тем самым двумерный поиск по всем парам \((a,b)\) сводится к конечному просмотру

$$p,\ p^2,\ p^3,\dots,\qquad\text{и для каждого из них }q,\ q^2,\ q^3,\dots$$

с остановкой в тот момент, когда произведение превысило бы \(N\).

Перебор пар простых чисел

После генерации всех простых до \(N\) решетом Эратосфена программа перебирает пары \(p \lt q\). Условие \(pq \le N\) уже полностью определяет, может ли пара дать ненулевой вклад. Поэтому

$$S(N)=\sum_{\substack{p \lt q\\ pq \le N}} M(p,q,N).$$

Внутренний цикл можно завершить, как только \(pq \gt N\), а внешний цикл завершается при \(2p \gt N\), поскольку для любого простого \(q \ge 2\). Это строгие математические отсечения.

Пример: \(N = 100\)

В условии приведены три полезные контрольные величины.

Для \((p,q)=(2,3)\) допустимые значения таковы:

$$6,\ 12,\ 18,\ 24,\ 36,\ 48,\ 54,\ 72,\ 96,$$

поэтому

$$M(2,3,100)=96.$$

Для \((p,q)=(3,5)\) получаем

$$15,\ 45,\ 75,$$

и, следовательно,

$$M(3,5,100)=75.$$

Для \((p,q)=(2,73)\) уже минимальное произведение равно \(2\cdot 73=146 \gt 100\), значит

$$M(2,73,100)=0.$$

Сумма по всем парам простых даёт контрольное значение

$$S(100)=2262.$$

Вывод структуры циклов, используемой в коде

Вспомогательная функция max_for_pair(p, q, limit) буквально реализует описанную выше математику. Сначала она перебирает

$$p,\ p^2,\ p^3,\dots,\ p^a \le \frac{N}{q},$$

потому что при \(p^a \gt N/q\) уже даже \(p^a q\) будет слишком большим. Для каждого такого \(p^a\) внутренний цикл стартует с \(p^a q\) и продолжает умножать на \(q\), пока ограничение не нарушено. Максимум среди всех посещённых значений и есть \(M(p,q,N)\).

Проверки вида value > limit / q перед умножением на \(q\) представляют собой безопасную целочисленную форму неравенства \(value \cdot q \le N\), предотвращающую переполнение.

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

Реализации на C++, Java и Python устроены одинаково и состоят из трёх этапов.

Сначала sieve_primes(limit) строит список простых до \(N\). Затем max_for_pair(p, q, limit) вычисляет \(M(p,q,N)\) перебором степеней. Наконец, основной алгоритм проходит по всем парам с \(pq \le N\) и суммирует найденные максимумы.

Дополнительно код хранит массив seen перед добавлением значения в сумму. С математической точки зрения это не обязательно: разные пары простых не могут породить одно и то же положительное число из-за единственности простого разложения. Значит, это лишь защитная проверка.

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

Решето Эратосфена требует \(O(N\log\log N)\) времени и \(O(N)\) памяти.

Для фиксированной пары \((p,q)\) перебор степеней в худшем случае выполняет \(O(\log_p N \cdot \log_q N)\) итераций; обычно меньше, поскольку циклы останавливаются сразу после превышения границы \(N\).

Итак, полная асимптотика равна

$$O\!\left(N\log\log N + \sum_{\substack{p \lt q\\ pq \le N}} \log_p N \cdot \log_q N\right),$$

а память составляет \(O(N)\) из-за решета и массива отметок. На практике это гораздо быстрее, чем разлагать на множители каждое число от \(1\) до \(N\).

Ссылки и литература

  1. Страница задачи: https://projecteuler.net/problem=347
  2. Решето Эратосфена: Wikipedia
  3. Основная теорема арифметики: Wikipedia
  4. Hardy, Wright: An Introduction to the Theory of Numbers.
  5. Логарифм: Wikipedia

ملخص المسألة

لكل زوج من عددين أوليين مختلفين \(p \lt q\)، نعرّف \(M(p,q,N)\) بأنه أكبر عدد لا يتجاوز \(N\) وتكون عوامله الأولية محصورة تمامًا في \(p\) و\(q\)، مع ظهور كل منهما مرة واحدة على الأقل. أي إن الأعداد المقبولة تأخذ الصورة \(p^a q^b\) حيث \(a,b \ge 1\). والمطلوب هو حساب

$$S(N)=\sum_{p \lt q} M(p,q,N).$$

جوهر المسألة هو إيجاد الأسس المثلى لكل زوج أولي من غير أن نضطر إلى تحليل كل عدد من \(1\) حتى \(N\) إلى عوامله الأولية.

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

لزوج أولي ثابت \(p,q\) لدينا

$$M(p,q,N)=\max\{p^a q^b : a,b \ge 1,\ p^a q^b \le N\},$$

ونتفق على أن \(M(p,q,N)=0\) إذا لم توجد أسس تحقق الشرط.

بنية الأعداد المسموح بها

بحسب النظرية الأساسية في الحساب، التحليل إلى عوامل أولية فريد. لذلك فإن كل عدد يُنسب إلى الزوج \((p,q)\) لا بد أن يكون من الشكل \(p^a q^b\) من دون أي عامل أولي ثالث. وأصغر عدد من هذا النوع هو \(pq\)، ومن ثم نحصل مباشرة على

$$pq \gt N \implies M(p,q,N)=0.$$

وهذه الملاحظة البسيطة هي التي تبرر الإنهاء المبكر في حلقتي الأعداد الأولية داخل الكود.

تحسين الأسس لزوج أولي ثابت

إذا ثبتنا \(p^a\)، فإن أكبر أس يسمح به للعدد \(q\) هو أكبر عدد صحيح \(b\) يحقق

$$q^b \le \frac{N}{p^a}.$$

وبصورة مكافئة،

$$b_{\max}(a)=\left\lfloor \log_q\!\left(\frac{N}{p^a}\right)\right\rfloor,$$

وذلك متى كان \(p^a q \le N\). وعند هذا الأس الثابت \(a\)، يكون أفضل مرشح هو

$$n_a = p^a q^{\,b_{\max}(a)}.$$

إذًا

$$M(p,q,N)=\max_{1 \le a \le \lfloor \log_p(N/q)\rfloor} n_a.$$

الكود لا يستخدم اللوغاريتمات العائمة فعليًا، بل يولّد قوى \(p\) و\(q\) عبر الضرب المتكرر، وهذا أدق ويتجنب مشاكل التقريب.

لماذا يكفي مسح الأسس

عندما يكون \(p^a\) ثابتًا، فكل القيم الممكنة هي من الشكل \(p^a q^b\). وبما أن \(q \gt 1\)، فإن القيمة تزداد بازدياد \(b\). لذا، عند تثبيت \(a\)، لا نحتاج إلا إلى أكبر قوة ممكنة لـ \(q\)؛ أما القوى الأصغر فلن تعطي قيمة أفضل.

وهكذا يتحول البحث ثنائي الأبعاد على جميع الأزواج \((a,b)\) إلى مسح محدود من النوع

$$p,\ p^2,\ p^3,\dots,\qquad\text{ولكل واحدة منها }q,\ q^2,\ q^3,\dots$$

مع التوقف فورًا عندما يصبح الضرب التالي أكبر من \(N\).

التكرار على جميع الأزواج الأولية

بعد توليد جميع الأعداد الأولية حتى \(N\) باستخدام غربال إراتوستينس، يمر البرنامج على الأزواج \(p \lt q\). والشرط \(pq \le N\) كافٍ لمعرفة ما إذا كان الزوج يمكن أن يساهم بقيمة غير صفرية. لذا فإن

$$S(N)=\sum_{\substack{p \lt q\\ pq \le N}} M(p,q,N).$$

تنتهي الحلقة الداخلية عندما يصبح \(pq \gt N\)، وتنتهي الحلقة الخارجية عندما يصبح \(2p \gt N\)، لأن كل عدد أولي يحقق \(q \ge 2\). وهذه حدود رياضية صحيحة وليست مجرد حيلة تنفيذية.

مثال عددي: \(N = 100\)

يوفر نص المسألة ثلاث قيم تحقق مفيدة.

بالنسبة إلى \((p,q)=(2,3)\)، فالقيم المسموح بها هي

$$6,\ 12,\ 18,\ 24,\ 36,\ 48,\ 54,\ 72,\ 96,$$

ومن ثم

$$M(2,3,100)=96.$$

وبالنسبة إلى \((p,q)=(3,5)\)، تكون القيم الممكنة

$$15,\ 45,\ 75,$$

وعليه

$$M(3,5,100)=75.$$

أما \((p,q)=(2,73)\)، فأصغر حاصل ضرب ممكن هو \(2\cdot 73=146 \gt 100\)، وبالتالي

$$M(2,73,100)=0.$$

وعند جمع القيم على جميع الأزواج الأولية نحصل على قيمة التحقق

$$S(100)=2262.$$

اشتقاق بنية الحلقات المستخدمة في الكود

الدالة المساعدة max_for_pair(p, q, limit) تطبق هذا الاشتقاق مباشرة. فهي تبدأ بمسح

$$p,\ p^2,\ p^3,\dots,\ p^a \le \frac{N}{q},$$

لأنه إذا صار \(p^a \gt N/q\)، فإن حتى \(p^a q\) سيكون أكبر من الحد. ولكل قيمة \(p^a\)، تبدأ الحلقة الداخلية من \(p^a q\) ثم تواصل الضرب في \(q\) ما دام الناتج لا يتجاوز الحد. وأكبر قيمة تُرى أثناء هذا المسح هي بالضبط \(M(p,q,N)\).

أما الشروط في الكود من نوع value > limit / q قبل الضرب في \(q\)، فهي مجرد صياغة صحيحة وآمنة عدديًا للشرط \(value \cdot q \le N\)، وتمنع تجاوز السعة.

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

حلول C++ وJava وPython تتبع البنية نفسها في ثلاث مراحل.

أولًا، تقوم sieve_primes(limit) بتوليد قائمة الأعداد الأولية حتى \(N\). ثانيًا، تحسب max_for_pair(p, q, limit) القيمة \(M(p,q,N)\) عبر مسح الأسس. ثالثًا، تمر الدالة الرئيسية على جميع الأزواج التي تحقق \(pq \le N\) وتجمع القيم العظمى الناتجة.

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

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

توليد الأعداد الأولية بالغربال يكلف \(O(N\log\log N)\) زمنًا و\(O(N)\) ذاكرة.

ولزوج ثابت \((p,q)\)، فإن مسح الأسس ينفذ في أسوأ الأحوال \(O(\log_p N \cdot \log_q N)\) تكرارًا، وعادة يكون أقل من ذلك لأن الحلقات تتوقف فور تجاوز \(N\).

إذًا يصبح الزمن الكلي

$$O\!\left(N\log\log N + \sum_{\substack{p \lt q\\ pq \le N}} \log_p N \cdot \log_q N\right),$$

مع ذاكرة \(O(N)\) للغربال ومصفوفة العلامات. وهذا أسرع بكثير عمليًا من تحليل كل عدد بين \(1\) و\(N\) إلى عوامله الأولية.

مراجع

  1. صفحة المسألة: https://projecteuler.net/problem=347
  2. غربال إراتوستينس: Wikipedia
  3. النظرية الأساسية في الحساب: Wikipedia
  4. Hardy, Wright: An Introduction to the Theory of Numbers.
  5. اللوغاريتم: Wikipedia