Problem Summary

In the implementation used in this repository, a distinct line is represented by a nonzero lattice direction \(P=(a,b,c)\) with \(0\le a,b,c\le N\). The line is the one passing through the origin and \(P\). Two lattice points describe the same line exactly when one is a positive integer multiple of the other, so each line has a unique primitive representative whose coordinates have gcd equal to 1.

Therefore the quantity being computed is

$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$

The solutions then print the exact decimal value if it has at most 18 digits, or the concatenation of the first 9 and last 9 digits when the full answer is much longer.

Mathematical Approach

Step 1: Distinct Lines Become Primitive Triples

If \(g=\gcd(a,b,c)\gt 1\), then \((a,b,c)=g(a',b',c')\) with \(\gcd(a',b',c')=1\). The points \((a,b,c)\) and \((a',b',c')\) lie on the same line through the origin, so the non-primitive vector is redundant. Conversely, every primitive triple inside the box gives one distinct line. Because the coordinates are restricted to \(\{0,\dots,N\}\), there is no sign ambiguity: each line contributes exactly one primitive direction vector.

Step 2: Möbius Inversion Enforces \(\gcd(a,b,c)=1\)

Use the standard coprimality indicator

$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d),$$

where \(\mu\) is the Möbius function. Summing this identity over all triples in the box yields

$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d).$$

Now exchange the order of summation. For a fixed divisor \(d\), the condition \(d\mid a,b,c\) means that each coordinate can be any multiple of \(d\) up to \(N\). That gives \(\left\lfloor N/d\right\rfloor+1\) choices per coordinate, including 0. The all-zero triple must then be removed exactly once.

Hence

$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$

This is the core formula implemented in the C++, Python, and Java files.

Step 3: Quotient Blocks Compress the Outer Sum

A naive loop over every \(d\le N\) is too slow when \(N=10^{10}\). The crucial observation is that the quotient

$$q=\left\lfloor\frac{N}{d}\right\rfloor$$

stays constant on whole intervals. If a block starts at \(l\), then every \(d\) in

$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$

has the same quotient \(q\). Over that interval the cubic factor is constant, so only the Möbius prefix sum changes. Introduce the Mertens function

$$M(x)=\sum_{n\le x}\mu(n).$$

Then for one block,

$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1),$$

and the full sum becomes a block sum of the form

$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right),$$

where the summation runs over all quotient blocks. The number of such blocks is only about \(2\sqrt N\), which is why the outer loop is practical.

Step 4: Fast Evaluation of the Mertens Prefix

The source files use the same two-level plan. First, they build \(\mu(d)\) with a linear sieve up to

$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right).$$

From this sieve they build the prefix array \(M(1),M(2),\dots,M(B)\).

For larger arguments, they use the classical identity

$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1,$$

which can be rewritten as

$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right).$$

Again, equal floor quotients are grouped, so one recursive subtraction handles a whole interval at once. Large values are cached after the first computation, exactly as the `cache` structure does in the implementations.

Worked Checkpoints

For \(N=1\), every nonzero triple in \(\{0,1\}^3\) is primitive, so

$$D(1)=7.$$

For \(N=2\), the Möbius formula already shows the mechanism:

$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$

The C++ code also checks the optimized result against brute force at \(N=40\), where both methods give

$$D(40)=56335.$$

A larger hard-coded checkpoint is

$$D(10^6)=831909254469114121,$$

and only after these tests does the program attempt the target \(N=10^{10}\).

How the Code Works

The C++ file is the authoritative implementation. The function mu_sieve constructs the linear Möbius sieve, the class MertensPrefix serves \(M(x)\) either from the prefix array or from the memoized recurrence, and distinct_lines performs the quotient-block accumulation. The main program stores the running total in __int128, exposes --n= and --skip-checkpoints, and formats the final output through first9_last9_token.

The Python file is a compact fixed-\(N\) version of the same mathematics and relies on Python's arbitrary-precision integers. The Java file mirrors the same algorithm, but uses BigInteger for the cubic block value and the final accumulated answer because Java has no built-in signed 128-bit integer type.

Complexity Analysis

Let \(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\). Building the linear sieve and the prefix Mertens table costs \(O(B)\) time and \(O(B)\) memory. The outer distinct-lines sum uses \(O(\sqrt N)\) quotient blocks. Large Mertens arguments are memoized and are themselves processed in grouped floor-quotient intervals, so the whole method is far below \(O(N)\) work and is practical for the implemented target \(N=10^{10}\).

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=388
  2. Möbius inversion formula: Wikipedia — Möbius inversion formula
  3. Möbius function: Wikipedia — Möbius function
  4. Mertens function: Wikipedia — Mertens function
  5. Quotient grouping and floor-sum methods: Wikipedia — Dirichlet hyperbola method

Problemzusammenfassung

In der hier verwendeten Implementierung wird jede verschiedene Linie durch einen nichtnulligen Gitterrichtungsvektor \(P=(a,b,c)\) mit \(0\le a,b,c\le N\) beschrieben. Gemeint ist die Gerade durch den Ursprung und \(P\). Zwei Gitterpunkte liefern genau dann dieselbe Gerade, wenn einer ein positives ganzzahliges Vielfaches des anderen ist. Daher besitzt jede Gerade genau einen primitiven Repräsentanten mit \(\gcd(a,b,c)=1\).

Gesucht ist also

$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$

Die Programme geben den exakten Dezimalwert aus, falls er höchstens 18 Stellen hat; andernfalls wird die Verkettung der ersten 9 und letzten 9 Stellen ausgegeben.

Mathematischer Ansatz

Schritt 1: Aus verschiedenen Geraden werden primitive Tripel

Ist \(g=\gcd(a,b,c)\gt 1\), dann gilt \((a,b,c)=g(a',b',c')\) mit \(\gcd(a',b',c')=1\). Die Punkte \((a,b,c)\) und \((a',b',c')\) liegen auf derselben Geraden durch den Ursprung, also ist der nichtprimitive Vektor nur ein mehrfach gezählter Vertreter. Umgekehrt bestimmt jedes primitive Tripel im Würfel genau eine Gerade. Weil alle Koordinaten in \(\{0,\dots,N\}\) liegen, gibt es keine Vorzeichenmehrdeutigkeit.

Schritt 2: Möbius-Inversion erzwingt \(\gcd(a,b,c)=1\)

Verwende die Standardidentität

$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d),$$

wobei \(\mu\) die Möbius-Funktion ist. Über alle Tripel im Würfel summiert ergibt das

$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d).$$

Nun wird die Summationsreihenfolge vertauscht. Für festes \(d\) bedeutet \(d\mid a,b,c\), dass jede Koordinate ein Vielfaches von \(d\) bis höchstens \(N\) ist. Das liefert \(\left\lfloor N/d\right\rfloor+1\) Möglichkeiten pro Koordinate, einschließlich 0. Das Nulltripel muss anschließend genau einmal entfernt werden.

Damit folgt

$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$

Genau diese Formel wird in allen drei Sprachversionen umgesetzt.

Schritt 3: Quotientenblöcke komprimieren die äußere Summe

Ein direkter Lauf über alle \(d\le N\) wäre für \(N=10^{10}\) zu langsam. Die zentrale Beobachtung ist, dass

$$q=\left\lfloor\frac{N}{d}\right\rfloor$$

auf ganzen Intervallen konstant bleibt. Beginnt ein Block bei \(l\), dann besitzen alle \(d\) in

$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$

denselben Quotienten \(q\). Auf diesem Intervall ist der kubische Faktor konstant; nur die Möbius-Präfixsumme ändert sich. Mit der Mertens-Funktion

$$M(x)=\sum_{n\le x}\mu(n)$$

gilt für einen Block

$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1),$$

und damit

$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right),$$

wobei über alle Quotientenblöcke summiert wird. Die Anzahl dieser Blöcke liegt nur bei etwa \(2\sqrt N\).

Schritt 4: Schnelle Auswertung der Mertens-Präfixe

Die Quelltexte benutzen dieselbe zweistufige Strategie. Zuerst wird \(\mu(d)\) mit einem linearen Sieb bis

$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)$$

berechnet. Daraus entsteht das Präfixarray \(M(1),M(2),\dots,M(B)\).

Für größere Argumente wird die klassische Identität

$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1$$

in die Rekursion

$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right)$$

umgeformt. Auch hier werden gleiche Floor-Quotienten zusammengefasst, sodass ein rekursiver Schritt jeweils ein ganzes Intervall verarbeitet. Bereits berechnete große Werte werden im Cache gehalten.

Kontrollbeispiele

Für \(N=1\) ist jedes von \((0,0,0)\) verschiedene Tripel in \(\{0,1\}^3\) bereits primitiv, also

$$D(1)=7.$$

Für \(N=2\) zeigt die Möbius-Formel direkt:

$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$

Die C++-Datei vergleicht außerdem den optimierten Weg mit Brute Force bei \(N=40\); beide liefern

$$D(40)=56335.$$

Ein weiterer fest eingebauter Prüfwert ist

$$D(10^6)=831909254469114121,$$

und erst danach wird das Ziel \(N=10^{10}\) berechnet.

Funktionsweise des Codes

Die C++-Datei ist die maßgebliche Implementierung. mu_sieve baut das lineare Möbius-Sieb, MertensPrefix liefert \(M(x)\) entweder direkt aus dem Präfixarray oder aus der memoisierten Rekursion, und distinct_lines akkumuliert die Quotientenblöcke. Das Hauptprogramm verwendet __int128 für die Gesamtsumme, akzeptiert --n= und --skip-checkpoints und formatiert das Ergebnis mit first9_last9_token.

Die Python-Datei ist eine kompakte Variante derselben Mathematik für festes \(N=10^{10}\) und nutzt die beliebig großen Ganzzahlen von Python. Die Java-Datei spiegelt denselben Algorithmus, verwendet für die großen Zwischenterme und die Endsumme aber BigInteger.

Komplexitätsanalyse

Mit \(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\) kosten lineares Sieb und Mertens-Präfixtabelle \(O(B)\) Zeit und \(O(B)\) Speicher. Die äußere Summation über verschiedene Quotientenblöcke benötigt \(O(\sqrt N)\) Blöcke. Große Mertens-Argumente werden gecacht und ebenfalls blockweise verarbeitet, sodass der Gesamtaufwand weit unter \(O(N)\) bleibt und für das implementierte Ziel \(N=10^{10}\) praktikabel ist.

Fußnoten und Quellen

  1. Problemseite: https://projecteuler.net/problem=388
  2. Möbius-Inversion: Wikipedia — Möbiusinversion
  3. Möbius-Funktion: Wikipedia — Möbiusfunktion
  4. Mertens-Funktion: Wikipedia — Mertens-Funktion
  5. Dirichletsche Hyperbelmethode: Wikipedia — Dirichletsche Hyperbelmethode

Problem Özeti

Bu depodaki uygulamada her farklı doğru, \(0\le a,b,c\le N\) aralığındaki sıfır olmayan bir \(P=(a,b,c)\) kafes yön vektörü ile temsil edilir. Kastedilen doğru, orijinden ve \(P\) noktasından geçen doğrudur. İki kafes noktası ancak biri diğerinin pozitif bir tam katıysa aynı doğruyu verir; bu yüzden her doğru, \(\gcd(a,b,c)=1\) olan tek bir ilkel temsilciye sahiptir.

Dolayısıyla hesaplanan nicelik

$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$

Çözüm dosyaları, sonuç 18 basamaktan kısaysa tamamını; daha uzunsa ilk 9 ve son 9 basamağın birleşimini verir.

Matematiksel Yaklaşım

Adım 1: Farklı Doğrular İlkel Üçlü Sayımına İner

Eğer \(g=\gcd(a,b,c)\gt 1\) ise \((a,b,c)=g(a',b',c')\) yazılabilir ve \(\gcd(a',b',c')=1\) olur. \((a,b,c)\) ile \((a',b',c')\) orijinden çıkan aynı doğru üzerindedir; yani ilkel olmayan vektör yalnızca aynı yönün başka bir temsilcisidir. Tersine, kutu içindeki her ilkel üçlü tam olarak bir doğruyu temsil eder. Tüm koordinatlar \(\{0,\dots,N\}\) içinde olduğundan işaret kaynaklı bir çift sayım yoktur.

Adım 2: Möbius Tersleme ile \(\gcd(a,b,c)=1\) Koşulu

Standart gösterge özdeşliği

$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d)$$

kullanılır; burada \(\mu\) Möbius fonksiyonudur. Bunu kutudaki tüm üçlüler üzerinde toplarsak

$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d)$$

elde edilir. Şimdi toplamların yerini değiştiririz. Sabit bir \(d\) için \(d\mid a,b,c\) koşulu, her koordinatın \(N\)'e kadar bir \(d\) katı olmasını söyler. Her koordinat için \(\left\lfloor N/d\right\rfloor+1\) seçenek vardır; buna 0 da dahildir. Sıfır üçlüsü ise bir kez çıkarılmalıdır.

Böylece

$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$

C++, Python ve Java dosyalarının tamamı tam olarak bu formülü uygular.

Adım 3: Eşit Bölüm Blokları ile Dış Toplamı Sıkıştırma

Tüm \(d\le N\) değerleri üzerinde tek tek dolaşmak \(N=10^{10}\) için fazla pahalıdır. Ana gözlem şudur:

$$q=\left\lfloor\frac{N}{d}\right\rfloor$$

değeri uzun aralıklarda sabit kalır. Bir blok \(l\) noktasında başlıyorsa,

$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$

aralığındaki tüm \(d\) değerleri aynı \(q\) bölümünü verir. Bu blokta kübik çarpan sabittir; değişen tek şey Möbius önek toplamıdır. Mertens fonksiyonunu

$$M(x)=\sum_{n\le x}\mu(n)$$

olarak tanımlarsak, blok toplamı

$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1)$$

olur ve tüm ifade

$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right)$$

şeklinde bloklar üzerinde yazılır. Farklı blok sayısı yaklaşık \(2\sqrt N\) mertebesindedir.

Adım 4: Mertens Öneklerinin Hızlı Hesabı

Kaynak dosyalar aynı iki katmanlı stratejiyi izler. Önce \(\mu(d)\),

$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)$$

sınırına kadar lineer elek ile üretilir. Ardından \(M(1),M(2),\dots,M(B)\) önek dizisi oluşturulur.

Daha büyük girdiler için klasik özdeşlik

$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1$$

şu rekürsiyona dönüştürülür:

$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right).$$

Burada da eşit bölüm değerleri birlikte işlendiği için tek bir özyineli adım bütün bir aralığı kapsar. Büyük \(M(x)\) değerleri ilk hesaplamadan sonra önbelleğe alınır.

Kontrol Noktaları

\(N=1\) için \(\{0,1\}^3\) içindeki sıfır olmayan her üçlü zaten ilkeldir; dolayısıyla

$$D(1)=7.$$

\(N=2\) için Möbius toplamı mekanizmayı açıkça gösterir:

$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$

C++ çözümü ayrıca \(N=40\) için optimize yöntemi kaba kuvvetle karşılaştırır ve her iki yol da

$$D(40)=56335$$

değerini verir. Kod içindeki bir başka güçlü kontrol değeri de

$$D(10^6)=831909254469114121$$

sonucudur.

Kodun Çalışma Mantığı

C++ dosyası ana referanstır. mu_sieve lineer Möbius eleğini kurar, MertensPrefix sınıfı \(M(x)\) değerini ya önek dizisinden ya da belleğe alınmış rekürsiyondan döndürür, distinct_lines ise bölüm blokları üzerinden toplamı hesaplar. Ana program toplamı __int128 içinde tutar, --n= ve --skip-checkpoints seçeneklerini destekler ve son çıktıyı first9_last9_token ile biçimlendirir.

Python dosyası aynı matematiğin sabit \(N=10^{10}\) için yazılmış daha kısa halidir ve Python'un sınırsız tamsayı tipini kullanır. Java dosyası aynı algoritmayı izler; ancak büyük kübik blok terimleri ve nihai toplam için BigInteger kullanır.

Karmaşıklık Analizi

\(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\) olsun. Lineer elek ve Mertens önek tablosu \(O(B)\) zaman ve \(O(B)\) bellek ister. Dış toplam eşit bölüm blokları sayesinde \(O(\sqrt N)\) blokta biter. Büyük Mertens sorguları önbellekli ve bloklanmış olduğundan toplam iş yükü \(O(N)\)'den çok daha küçüktür ve uygulanmış hedef \(N=10^{10}\) için pratiktir.

Dipnotlar ve Kaynakça

  1. Problem sayfası: https://projecteuler.net/problem=388
  2. Möbius tersleme formülü: Wikipedia — Möbius inversion formula
  3. Möbius fonksiyonu: Wikipedia — Möbius fonksiyonu
  4. Mertens fonksiyonu: Wikipedia — Mertens fonksiyonu
  5. Dirichlet hiperbol yöntemi: Wikipedia — Dirichlet hyperbola method

Resumen del Problema

En la implementación de este repositorio, cada línea distinta se representa mediante un vector de dirección de retícula no nulo \(P=(a,b,c)\) con \(0\le a,b,c\le N\). La línea es la que pasa por el origen y por \(P\). Dos puntos de la retícula describen la misma línea exactamente cuando uno es un múltiplo entero positivo del otro; por eso, cada línea tiene un único representante primitivo con \(\gcd(a,b,c)=1\).

La cantidad que se calcula es, por tanto,

$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$

Los programas imprimen el valor decimal completo si tiene como máximo 18 cifras; en caso contrario, devuelven la concatenación de las primeras 9 y las últimas 9 cifras.

Enfoque Matemático

Paso 1: Las líneas distintas se convierten en ternas primitivas

Si \(g=\gcd(a,b,c)\gt 1\), entonces \((a,b,c)=g(a',b',c')\) con \(\gcd(a',b',c')=1\). Los puntos \((a,b,c)\) y \((a',b',c')\) están en la misma línea que sale del origen, así que el vector no primitivo no aporta una línea nueva. A la inversa, cada terna primitiva dentro del cubo determina exactamente una línea. Como todas las coordenadas están en \(\{0,\dots,N\}\), no hay ambigüedad de signo.

Paso 2: La inversión de Möbius impone \(\gcd(a,b,c)=1\)

Usamos la identidad estándar

$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d),$$

donde \(\mu\) es la función de Möbius. Al sumar esta identidad sobre todas las ternas del cubo obtenemos

$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d).$$

Ahora intercambiamos el orden de sumación. Para un \(d\) fijo, la condición \(d\mid a,b,c\) significa que cada coordenada puede ser cualquier múltiplo de \(d\) no mayor que \(N\). Eso da \(\left\lfloor N/d\right\rfloor+1\) opciones por coordenada, incluyendo el 0. La terna nula debe restarse exactamente una vez.

Así llegamos a

$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$

Esta es la fórmula exacta que usan las tres implementaciones.

Paso 3: Agrupar por cocientes iguales

Recorrer todos los \(d\le N\) sigue siendo demasiado lento para \(N=10^{10}\). La observación decisiva es que

$$q=\left\lfloor\frac{N}{d}\right\rfloor$$

permanece constante en intervalos completos. Si un bloque empieza en \(l\), todos los \(d\) en

$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$

comparten el mismo cociente \(q\). En ese bloque, el factor cúbico es constante y solo cambia la suma prefijo de Möbius. Definimos la función de Mertens

$$M(x)=\sum_{n\le x}\mu(n).$$

Entonces, dentro de un bloque,

$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1),$$

y la suma completa se reescribe como

$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right),$$

donde la sumatoria recorre todos los bloques de cociente. El número de bloques es solo del orden de \(2\sqrt N\).

Paso 4: Cálculo rápido del prefijo de Mertens

Los archivos fuente usan el mismo plan en dos niveles. Primero calculan \(\mu(d)\) con una criba lineal hasta

$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right).$$

Con eso forman el arreglo prefijo \(M(1),M(2),\dots,M(B)\).

Para argumentos mayores utilizan la identidad clásica

$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1,$$

que se transforma en la recurrencia

$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right).$$

De nuevo, los valores iguales de la parte entera se agrupan, así que una sola llamada recursiva procesa un intervalo completo. Los valores grandes de \(M(x)\) se memorizan tras el primer cálculo.

Comprobaciones de ejemplo

Para \(N=1\), toda terna no nula de \(\{0,1\}^3\) ya es primitiva; por tanto,

$$D(1)=7.$$

Para \(N=2\), la fórmula de Möbius muestra el mecanismo de inmediato:

$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$

El código en C++ además compara el método optimizado con fuerza bruta en \(N=40\), y ambos dan

$$D(40)=56335.$$

Otro punto de control incorporado es

$$D(10^6)=831909254469114121,$$

antes de atacar el objetivo \(N=10^{10}\).

Cómo Funciona el Código

El archivo C++ es la implementación principal. mu_sieve construye la criba lineal de Möbius, la clase MertensPrefix devuelve \(M(x)\) desde el arreglo prefijo o desde la recurrencia memorizada, y distinct_lines acumula la suma por bloques de cociente. El programa principal usa __int128, acepta --n= y --skip-checkpoints, y da formato a la salida con first9_last9_token.

El archivo Python es una versión compacta del mismo algoritmo para \(N=10^{10}\) fijo y aprovecha los enteros de precisión arbitraria del lenguaje. El archivo Java sigue la misma matemática, pero utiliza BigInteger para el término cúbico de cada bloque y para la suma final.

Análisis de Complejidad

Sea \(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\). La criba lineal y la tabla prefijo de Mertens cuestan \(O(B)\) tiempo y \(O(B)\) memoria. La suma exterior usa \(O(\sqrt N)\) bloques de cociente. Las consultas grandes de Mertens quedan memorizadas y también se procesan por bloques de partes enteras, así que el coste total está muy por debajo de \(O(N)\) y resulta práctico para el objetivo implementado \(N=10^{10}\).

Referencias

  1. Problema: https://projecteuler.net/problem=388
  2. Inversión de Möbius: Wikipedia — inversión de Möbius
  3. Función de Möbius: Wikipedia — función de Möbius
  4. Función de Mertens: Wikipedia — función de Mertens
  5. Método hiperbólico de Dirichlet: Wikipedia — método hiperbólico de Dirichlet

问题概述

在这个仓库的实现里,每一条“不同的直线”都用一个非零格点方向向量 \(P=(a,b,c)\) 表示,其中 \(0\le a,b,c\le N\)。这条线就是经过原点和 \(P\) 的直线。两个格点恰好在其中一个是另一个的正整数倍时 表示同一条直线,因此每条直线都对应唯一的原始代表,也就是满足 \(\gcd(a,b,c)=1\) 的方向向量。

所以实际计算的量是

$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$

如果答案的十进制长度不超过 18 位,程序输出完整结果;否则输出前 9 位与后 9 位的拼接。

数学推导

步骤 1:把不同直线转化为原始三元组计数

若 \(g=\gcd(a,b,c)\gt 1\),则可以写成 \((a,b,c)=g(a',b',c')\),并且 \(\gcd(a',b',c')=1\)。点 \((a,b,c)\) 与 \((a',b',c')\) 落在同一条经过原点的直线上,所以非原始向量并不会贡献新的直线。反过来, 盒子中的每个原始三元组都唯一对应一条直线。由于所有坐标都限制在 \(\{0,\dots,N\}\) 内,这里不存在符号 对称带来的重复计数。

步骤 2:用 Möbius 反演刻画 \(\gcd(a,b,c)=1\)

使用标准指示函数恒等式

$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d),$$

其中 \(\mu\) 是 Möbius 函数。把它对立方体中的所有三元组求和,得到

$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d).$$

接着交换求和顺序。对固定的 \(d\),条件 \(d\mid a,b,c\) 表示每个坐标都必须是 \(d\) 的倍数且不超过 \(N\)。每个坐标因此有 \(\left\lfloor N/d\right\rfloor+1\) 种选择,其中包含 0。最后再把零三元组减去一次。

于是得到

$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$

这就是 C++、Python 和 Java 三份代码共同实现的核心公式。

步骤 3:按相同商值分块

即使有了闭式,逐个遍历所有 \(d\le N\) 仍然太慢。关键观察是

$$q=\left\lfloor\frac{N}{d}\right\rfloor$$

在一整段区间上保持不变。若当前块从 \(l\) 开始,则所有满足

$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$

的 \(d\) 都有相同的商 \(q\)。在这个区间内,立方因子不变,变化的只有 Möbius 前缀和。定义 Mertens 函数

$$M(x)=\sum_{n\le x}\mu(n).$$

于是一个区间上的 Möbius 和为

$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1),$$

整体求和可改写为

$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right),$$

其中求和遍历所有商值分块。这样的块数大约只有 \(2\sqrt N\) 个。

步骤 4:快速求 Mertens 前缀

三份源码都采用相同的两层策略。先用线性筛求出 \(\mu(d)\),筛到

$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right).$$

然后构造前缀数组 \(M(1),M(2),\dots,M(B)\)。

对更大的参数,使用经典恒等式

$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1,$$

并改写为递推式

$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right).$$

这里同样按相同整除商分组,因此一次递归就能处理整段区间。大参数的 \(M(x)\) 会在首次计算后写入缓存, 这正对应源码中的缓存字典或映射。

校验示例

当 \(N=1\) 时,\(\{0,1\}^3\) 中除 \((0,0,0)\) 之外的每个三元组都是原始的,因此

$$D(1)=7.$$

当 \(N=2\) 时,Möbius 公式可以直接算出

$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$

C++ 程序还会在 \(N=40\) 上把优化算法与暴力枚举比较,两者都得到

$$D(40)=56335.$$

源码中的另一个较大检查点是

$$D(10^6)=831909254469114121,$$

随后才计算目标值 \(N=10^{10}\)。

代码说明

C++ 文件是权威实现。mu_sieve 构造线性 Möbius 筛,MertensPrefix 类从前缀表 或记忆化递推中返回 \(M(x)\),distinct_lines 则完成按商分块的主求和。主程序使用 __int128 保存总和,支持 --n=--skip-checkpoints,最后通过 first9_last9_token 格式化输出。

Python 文件是同一数学思路的紧凑版本,固定求解 \(N=10^{10}\),并直接利用 Python 的大整数。Java 文件 保持同样的算法流程,但由于没有内建的 128 位有符号整数,所以用 BigInteger 保存块值和最终答案。

复杂度分析

令 \(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\)。线性筛和 Mertens 前缀表需要 \(O(B)\) 时间与 \(O(B)\) 空间。外层不同商值的求和只需 \(O(\sqrt N)\) 个分块。较大的 Mertens 查询会 被缓存,而且递推本身也按相同商值分段处理,所以总工作量远小于 \(O(N)\),足以支持实现中的 \(N=10^{10}\)。

参考资料

  1. 题目页面: https://projecteuler.net/problem=388
  2. Möbius 反演公式: Wikipedia — Möbius inversion formula
  3. Möbius 函数: Wikipedia — Möbius 函数
  4. Mertens 函数: Wikipedia — Mertens 函数
  5. Dirichlet 双曲线方法: Wikipedia — Dirichlet hyperbola method

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

В реализации этого репозитория каждая различная прямая задаётся ненулевым решеточным вектором направления \(P=(a,b,c)\) при \(0\le a,b,c\le N\). Речь идёт о прямой, проходящей через начало координат и точку \(P\). Две точки задают одну и ту же прямую тогда и только тогда, когда одна является положительным целым кратным другой. Значит, у каждой прямой есть единственный примитивный представитель с \(\gcd(a,b,c)=1\).

Итак, программа вычисляет

$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$

Если десятичная запись результата имеет не более 18 цифр, выводится всё число; иначе печатается конкатенация первых 9 и последних 9 цифр.

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

Шаг 1: Различные прямые равносильны примитивным тройкам

Если \(g=\gcd(a,b,c)\gt 1\), то \((a,b,c)=g(a',b',c')\) и \(\gcd(a',b',c')=1\). Точки \((a,b,c)\) и \((a',b',c')\) лежат на одной и той же прямой через начало координат, поэтому непримитивный вектор лишь повторяет уже имеющееся направление. Обратно, каждая примитивная тройка внутри куба задаёт ровно одну прямую. Поскольку все координаты неотрицательны, неоднозначности по знаку здесь нет.

Шаг 2: Инверсия Мёбиуса выделяет условие \(\gcd(a,b,c)=1\)

Используем стандартную формулу

$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d),$$

где \(\mu\) обозначает функцию Мёбиуса. Суммируя по всем тройкам в кубе, получаем

$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d).$$

Теперь меняем порядок суммирования. Для фиксированного \(d\) условие \(d\mid a,b,c\) означает, что каждая координата является кратной \(d\) и не превосходит \(N\). Это даёт \(\left\lfloor N/d\right\rfloor+1\) вариантов на координату, включая 0. Нулевую тройку затем нужно вычесть один раз.

Следовательно,

$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$

Именно эту формулу реализуют все три версии решения.

Шаг 3: Блочная группировка по одинаковому частному

Перебирать все \(d\le N\) всё ещё слишком дорого для \(N=10^{10}\). Ключевое наблюдение таково:

$$q=\left\lfloor\frac{N}{d}\right\rfloor$$

остаётся постоянным на целых интервалах. Если блок начинается в точке \(l\), то все \(d\) из диапазона

$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$

имеют один и тот же частный \(q\). На таком блоке кубический множитель постоянен, а меняется только префиксная сумма Möbius. Введём функцию Мертенса

$$M(x)=\sum_{n\le x}\mu(n).$$

Тогда сумма Möbius на блоке равна

$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1),$$

и общая формула переписывается как

$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right),$$

где суммирование ведётся по всем блокам одинакового частного. Число блоков имеет порядок \(2\sqrt N\).

Шаг 4: Быстрое вычисление префиксов Мертенса

Исходные файлы используют одинаковую двухуровневую схему. Сначала функция \(\mu(d)\) строится линейным ситом до

$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right).$$

После этого формируется массив префиксов \(M(1),M(2),\dots,M(B)\).

Для больших аргументов используется классическое тождество

$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1,$$

которое переписывается в рекуррентный вид

$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right).$$

Здесь снова группируются одинаковые значения целой части, поэтому один рекурсивный шаг обрабатывает целый интервал. Крупные значения \(M(x)\) после первого вычисления сохраняются в кэше.

Проверочные значения

При \(N=1\) каждая ненулевая тройка из \(\{0,1\}^3\) уже примитивна, поэтому

$$D(1)=7.$$

При \(N=2\) формула Мёбиуса сразу даёт

$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$

Кроме того, C++-код сравнивает быстрый метод с полным перебором при \(N=40\); оба способа дают

$$D(40)=56335.$$

Ещё одна встроенная контрольная точка:

$$D(10^6)=831909254469114121.$$

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

Файл C++ является основной реализацией. Функция mu_sieve строит линейное сито Мёбиуса, класс MertensPrefix возвращает \(M(x)\) либо из префиксного массива, либо из мемоизированной рекурсии, а distinct_lines выполняет суммирование по блокам одинакового частного. Главная функция хранит итог в типе __int128, поддерживает опции --n= и --skip-checkpoints и форматирует вывод через first9_last9_token.

Файл Python является компактной версией того же алгоритма для фиксированного \(N=10^{10}\) и использует произвольную точность целых чисел Python. Файл Java повторяет ту же математику, но применяет BigInteger для кубического значения блока и для окончательной суммы.

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

Пусть \(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\). Линейное сито и префиксная таблица Мертенса требуют \(O(B)\) времени и \(O(B)\) памяти. Внешняя сумма по блокам одинакового частного использует \(O(\sqrt N)\) блоков. Большие запросы к \(M(x)\) кэшируются и тоже обрабатываются блоками по одинаковым значениям целой части, поэтому общий объём работы существенно меньше \(O(N)\) и остаётся практичным для \(N=10^{10}\).

Ссылки

  1. Условие задачи: https://projecteuler.net/problem=388
  2. Формула инверсии Мёбиуса: Wikipedia — Möbius inversion formula
  3. Функция Мёбиуса: Wikipedia — функция Мёбиуса
  4. Функция Мертенса: Wikipedia — функция Мертенса
  5. Метод Дирихле гиперболы: Wikipedia — Dirichlet hyperbola method

ملخص المسألة

في التطبيق الموجود في هذا المستودع تُمثَّل كل مستقيم مميز بمتجه شبكي غير صفري \(P=(a,b,c)\) حيث \(0\le a,b,c\le N\). المقصود هو المستقيم المار بالمبدأ وبالنقطة \(P\). نقطتان شبكيتان تعطيان المستقيم نفسه بالضبط عندما تكون إحداهما مضاعفًا صحيحًا موجبًا للأخرى، ولذلك لكل مستقيم ممثل بدائي وحيد يحقق \(\gcd(a,b,c)=1\).

إذًا الكمية المحسوبة هي

$$D(N)=\#\left\{(a,b,c)\in \{0,\dots,N\}^3\setminus\{(0,0,0)\}:\gcd(a,b,c)=1\right\}.$$

إذا كان الجواب لا يتجاوز 18 خانة عشرية تُطبع القيمة كاملة، وإلا تُطبع أول 9 خانات وآخر 9 خانات ملتصقة كما تفعل ملفات الحلول.

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

الخطوة 1: تحويل المستقيمات المختلفة إلى ثلاثيات بدائية

إذا كان \(g=\gcd(a,b,c)\gt 1\)، فلدينا \((a,b,c)=g(a',b',c')\) مع \(\gcd(a',b',c')=1\). النقطتان \((a,b,c)\) و\((a',b',c')\) تقعان على المستقيم نفسه الخارج من المبدأ، لذا فالمتجه غير البدائي ليس إلا نسخة مكررة من الاتجاه نفسه. وبالعكس، كل ثلاثي بدائي داخل الصندوق يحدد مستقيمًا واحدًا بالضبط. وبما أن جميع الإحداثيات محصورة في \(\{0,\dots,N\}\)، فلا توجد هنا مشكلة ازدواج الإشارة.

الخطوة 2: عكس موبيوس لفرض الشرط \(\gcd(a,b,c)=1\)

نستخدم الهوية القياسية

$$\mathbf{1}_{\gcd(a,b,c)=1}=\sum_{d\mid \gcd(a,b,c)} \mu(d),$$

حيث \(\mu\) هي دالة موبيوس. وعند جمعها على جميع الثلاثيات داخل المكعب نحصل على

$$D(N)=\sum_{0\le a,b,c\le N}\mathbf{1}_{(a,b,c)\neq(0,0,0)}\sum_{d\mid \gcd(a,b,c)}\mu(d).$$

الآن نبدل ترتيب الجمع. عندما يكون \(d\) ثابتًا فإن الشرط \(d\mid a,b,c\) يعني أن كل إحداثي يمكن أن يكون أي مضاعف لـ \(d\) لا يتجاوز \(N\). وهذا يعطي \(\left\lfloor N/d\right\rfloor+1\) اختيارًا لكل إحداثي، بما في ذلك الصفر. ثم يجب حذف الثلاثي الصفري مرة واحدة فقط.

ومن ثم نحصل على

$$\boxed{D(N)=\sum_{d=1}^{N}\mu(d)\left(\left(\left\lfloor\frac{N}{d}\right\rfloor+1\right)^3-1\right).}$$

وهذه هي الصيغة الأساسية المطبقة في ملفات C++ وPython وJava.

الخطوة 3: تجميع الحدود ذات خارج القسمة نفسه

حتى بعد هذه الصيغة يبقى المرور على كل \(d\le N\) بطيئًا جدًا عندما \(N=10^{10}\). الفكرة الحاسمة هي أن

$$q=\left\lfloor\frac{N}{d}\right\rfloor$$

يبقى ثابتًا على فترات كاملة. إذا بدأنا عند \(l\)، فإن جميع قيم \(d\) في

$$l\le d\le r=\left\lfloor\frac{N}{q}\right\rfloor$$

تشترك في القيمة نفسها لـ \(q\). في هذا المقطع يكون العامل التكعيبي ثابتًا، والمتغير الوحيد هو مجموع موبيوس التراكمي. نعرّف دالة ميرتنز

$$M(x)=\sum_{n\le x}\mu(n).$$

وعندئذٍ يصبح مجموع موبيوس على المقطع

$$\sum_{d=l}^{r}\mu(d)=M(r)-M(l-1),$$

فتتحول الصيغة كلها إلى

$$D(N)=\sum \left((q+1)^3-1\right)\left(M(r)-M(l-1)\right),$$

حيث يمتد الجمع على جميع مقاطع خارج القسمة. وعدد هذه المقاطع يقارب \(2\sqrt N\) فقط.

الخطوة 4: حساب سريع لقيم ميرتنز التراكمية

تتبع ملفات الشيفرة الخطة نفسها على مستويين. أولًا تُبنى دالة موبيوس بخوارزمية غربال خطي حتى الحد

$$B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right).$$

ومن هذا الغربال تُنشأ المصفوفة التراكمية \(M(1),M(2),\dots,M(B)\).

أما للقيم الأكبر فتُستخدم الهوية الكلاسيكية

$$\sum_{d=1}^{x}\mu(d)\left\lfloor\frac{x}{d}\right\rfloor=1,$$

والتي يمكن إعادة كتابتها على الصورة

$$M(x)=1-\sum_{k=2}^{x} M\left(\left\lfloor\frac{x}{k}\right\rfloor\right).$$

وهنا أيضًا تُجمع القيم ذات خارج القسمة المتساوي، بحيث تعالج خطوة عودية واحدة فترة كاملة. وبعد أول حساب للقيمة الكبيرة \(M(x)\) تُخزن في الذاكرة المؤقتة تمامًا كما في بنية cache داخل الحلول.

نقاط فحص عملية

عندما \(N=1\)، فإن كل ثلاثي غير صفري في \(\{0,1\}^3\) بدائي بالفعل، لذلك

$$D(1)=7.$$

وعندما \(N=2\)، تظهر آلية الصيغة مباشرة:

$$D(2)=\mu(1)\left((2+1)^3-1\right)+\mu(2)\left((1+1)^3-1\right)=26-7=19.$$

كما أن شيفرة C++ تتحقق من التطابق مع العد الكامل عند \(N=40\)، وفي الحالتين تكون النتيجة

$$D(40)=56335.$$

ومن نقاط التحقق الأكبر داخل المصدر

$$D(10^6)=831909254469114121,$$

ثم بعد ذلك فقط يُحسب الهدف \(N=10^{10}\).

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

ملف C++ هو المرجع الأساسي. الدالة mu_sieve تبني غربال موبيوس الخطي، والصنف MertensPrefix يعيد \(M(x)\) إما من مصفوفة السوابق أو من العودية المحفوظة، والدالة distinct_lines تجمع المساهمة على مقاطع خارج القسمة. البرنامج الرئيسي يخزن المجموع في النوع __int128، ويدعم الخيارين --n= و--skip-checkpoints، ثم ينسق الجواب عبر first9_last9_token.

ملف Python هو نسخة موجزة من الفكرة نفسها عند \(N=10^{10}\) ثابت، ويستفيد من الأعداد الصحيحة غير المحدودة في بايثون. أما ملف Java فيطبق الرياضيات نفسها، لكنه يستخدم BigInteger لقيم المقاطع التكعيبية وللمجموع النهائي.

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

إذا وضعنا \(B=\max\left(10^6,\left\lfloor N^{2/3}\right\rfloor+1000\right)\)، فإن بناء الغربال الخطي وجدول ميرتنز التراكمي يكلف \(O(B)\) زمنًا و\(O(B)\) ذاكرة. المجموع الخارجي يحتاج إلى \(O(\sqrt N)\) مقاطع فقط. أما استعلامات ميرتنز الكبيرة فتُحفظ وتُحسب هي أيضًا على مقاطع ذات خارج قسمة متساوٍ، لذلك يبقى العمل الكلي أقل بكثير من \(O(N)\)، وهذا ما يجعل الهدف المطبق \(N=10^{10}\) ممكنًا عمليًا.

مراجع

  1. صفحة المسألة: https://projecteuler.net/problem=388
  2. عكس موبيوس: Wikipedia — Möbius inversion formula
  3. دالة موبيوس: Wikipedia — دالة موبيوس
  4. دالة ميرتنز: Wikipedia — Mertens function
  5. طريقة ديريشليه الزائدية: Wikipedia — Dirichlet hyperbola method