Problem Summary

Problem 559 asks for the value of \(Q(50000)\) modulo the prime

$$M=1000000123.$$

The matrix-counting statement is encoded through auxiliary quantities \(P(k,r,n)\). The C++, Python, and Java implementations never try to enumerate the matrices directly. Instead, they transform the count into a coefficient-extraction problem built from inverse factorial powers, alternating signs, and one outer sum over \(k\).

Mathematical Approach

Fix \(n\), \(r\), and \(k\). The implementations first compute a normalized core value and multiply by \((n!)^r\) only at the end. That normalization turns the combinatorial problem into a clean reciprocal-series recurrence.

Step 1: Normalize by factorial powers

Because \(M\) is prime and the computation only needs factorials up to \(n=50000\lt M\), every \(m!\) is invertible modulo \(M\). Define

$$u_m=(m!)^{-r}\pmod{M}\qquad (0\le m\le n).$$

These numbers are the basic weights of the method. All divisions by factorials are replaced by modular inverses, and the missing factor \((n!)^r\) is restored only after the normalized coefficient has been computed.

Step 2: Split \(n\) into full \(k\)-blocks and a remainder

Write

$$n=bk+s,\qquad b=\left\lfloor\frac{n}{k}\right\rfloor,\qquad 0\le s\lt k.$$

The recurrence advances in jumps of size \(k\). The quotient \(b\) counts how many full \(k\)-steps fit into \(n\), while the remainder \(s\) records the final incomplete part. This is why the formulas separate naturally into a full-block part and a remainder correction.

Step 3: Build the reciprocal power series

Introduce the formal power series

$$F_k(z)=\sum_{j\ge 0}(-1)^j u_{jk}z^j=1-u_k z+u_{2k}z^2-u_{3k}z^3+\cdots.$$

Now define its reciprocal

$$A_k(z)=\frac{1}{F_k(z)}=\sum_{i\ge 0} a_i z^i.$$

Comparing coefficients in \(A_k(z)F_k(z)=1\) gives

$$a_0=1,$$

$$a_i=\sum_{j=1}^{i}(-1)^{j+1}a_{i-j}u_{jk}\qquad (i\ge 1).$$

This alternating convolution is the heart of the algorithm. Once the sequence \(a_0,a_1,\dots,a_b\) is known, the rest is just a short finishing sum.

Step 4: Correct the final partial block

If \(s=0\), the normalized core contribution is simply

$$\gamma(n,k,r)=a_b.$$

If \(s\gt 0\), one shifted series is needed:

$$G_{k,s}(z)=\sum_{j\ge 0}(-1)^j u_{s+jk}z^j.$$

The desired core is then the coefficient of \(z^b\) in the product \(A_k(z)G_{k,s}(z)\):

$$\gamma(n,k,r)=[z^b](A_k(z)G_{k,s}(z))=\sum_{j=0}^{b}(-1)^j a_{b-j}u_{s+jk}.$$

So the same recurrence handles all complete \(k\)-blocks, and the shifted tail series accounts for the leftover \(s\) entries.

Step 5: Recover \(P(k,r,n)\) and assemble \(Q(n)\)

After the normalized core has been computed, the required count is

$$P(k,r,n)=(n!)^r\gamma(n,k,r)\pmod{M}.$$

For the Project Euler target, the implementations set \(r=n\) and sum over every \(k\):

$$Q(n)=(n!)^n\sum_{k=1}^{n}\gamma(n,k,n)\pmod{M}.$$

The full problem is therefore reduced to evaluating one reciprocal-series coefficient for each \(k\), summing those normalized cores, and multiplying once at the end.

Worked Example: \(P(1,2,3)=19\)

Take \(k=1\), \(r=2\), and \(n=3\). Then \(b=3\) and \(s=0\). The relevant weights are

$$u_1=\frac{1}{(1!)^2}=1,\qquad u_2=\frac{1}{(2!)^2}=\frac14,\qquad u_3=\frac{1}{(3!)^2}=\frac1{36}.$$

Now apply the recurrence:

$$a_0=1,$$

$$a_1=a_0u_1=1,$$

$$a_2=a_1u_1-a_0u_2=1-\frac14=\frac34,$$

$$a_3=a_2u_1-a_1u_2+a_0u_3=\frac34-\frac14+\frac1{36}=\frac{19}{36}.$$

Because \(s=0\), we have \(\gamma(3,1,2)=a_3\). Therefore

$$P(1,2,3)=(3!)^2\cdot \frac{19}{36}=36\cdot \frac{19}{36}=19,$$

which matches the built-in checkpoint used by the implementations.

How the Code Works

The C++, Python, and Java implementations follow the same mathematics. They first precompute factorials and inverse factorials, then raise each inverse factorial to the exponent \(r\) to obtain the table \(u_m\). For a fixed \(k\), they fill the coefficient sequence \(a_0,a_1,\dots,a_b\) with the alternating convolution above and evaluate the shifted finishing sum when \(s>0\).

To compute \(P(k,r,n)\), the implementation multiplies the normalized core by \((n!)^r\). To compute \(Q(n)\), it uses \(r=n\), repeats the same per-\(k\) core computation for every \(1\le k\le n\), adds the cores, and only then multiplies by \((n!)^n\). The C++ version parallelizes the outer loop over \(k\), the Java version runs the same logic serially, and the Python version acts as a thin bridge that invokes the compiled C++ solver and returns its numeric result.

Complexity Analysis

Precomputing factorials and inverse factorials costs \(O(n)\). Building the table \(u_m=(m!)^{-r}\) with repeated fast exponentiation costs \(O(n\log r)\). For one fixed \(k\), the recurrence length is \(b=\lfloor n/k\rfloor\), and the nested alternating sum costs \(O(b^2)\) time. Therefore

$$\sum_{k=1}^{n} O\!\left(\left\lfloor\frac{n}{k}\right\rfloor^2\right)=O(n^2),$$

so \(Q(n)\) is evaluated in overall \(O(n^2)\) time. The main tables use \(O(n)\) memory; the multithreaded C++ version adds one reusable work array per worker thread.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=559
  2. Permutation: Wikipedia — Permutation
  3. Inclusion-exclusion principle: Wikipedia — Inclusion-exclusion principle
  4. Formal power series: Wikipedia — Formal power series
  5. Modular multiplicative inverse: Wikipedia — Modular multiplicative inverse

Problemzusammenfassung

Problem 559 verlangt den Wert von \(Q(50000)\) modulo der Primzahl

$$M=1000000123.$$

Die eigentliche Matrixzählung wird über Hilfsgrößen \(P(k,r,n)\) formuliert. Die C++, Python- und Java-Implementierungen enumerieren die Matrizen nicht direkt, sondern wandeln die Aufgabe in ein Koeffizientenproblem mit inversen Fakultätspotenzen, alternierenden Vorzeichen und einer äußeren Summe über \(k\) um.

Mathematischer Ansatz

Fixiere \(n\), \(r\) und \(k\). Zuerst wird ein normierter Kernwert berechnet; der Faktor \((n!)^r\) kommt erst am Ende zurück. Dadurch wird die kombinatorische Struktur zu einer sauberen Rekursion für eine reziproke Reihe.

Schritt 1: Mit Fakultätspotenzen normieren

Da \(M\) prim ist und nur Fakultäten bis \(n=50000\lt M\) benötigt werden, ist jedes \(m!\) modulo \(M\) invertierbar. Definiere

$$u_m=(m!)^{-r}\pmod{M}\qquad (0\le m\le n).$$

Diese Werte sind die Grundgewichte der Methode. Alle Divisionen durch Fakultäten werden durch modulare Inverse ersetzt, und der fehlende Faktor \((n!)^r\) wird erst nach der normierten Koeffizientenberechnung wieder eingesetzt.

Schritt 2: \(n\) in volle \(k\)-Blöcke und einen Rest zerlegen

Schreibe

$$n=bk+s,\qquad b=\left\lfloor\frac{n}{k}\right\rfloor,\qquad 0\le s\lt k.$$

Die Rekursion springt immer in Schritten der Größe \(k\). Der Quotient \(b\) zählt also die vollständigen \(k\)-Schritte, und der Rest \(s\) beschreibt den letzten unvollständigen Teil. Deshalb zerfallen die Formeln natürlich in einen Vollblock-Teil und eine Restkorrektur.

Schritt 3: Die reziproke Potenzreihe aufbauen

Führe die formale Potenzreihe

$$F_k(z)=\sum_{j\ge 0}(-1)^j u_{jk}z^j=1-u_k z+u_{2k}z^2-u_{3k}z^3+\cdots$$

ein. Definiere dann ihre Kehrreihe

$$A_k(z)=\frac{1}{F_k(z)}=\sum_{i\ge 0} a_i z^i.$$

Aus dem Koeffizientenvergleich in \(A_k(z)F_k(z)=1\) folgt

$$a_0=1,$$

$$a_i=\sum_{j=1}^{i}(-1)^{j+1}a_{i-j}u_{jk}\qquad (i\ge 1).$$

Diese alternierende Faltung ist der Kern des Algorithmus. Sobald \(a_0,a_1,\dots,a_b\) bekannt sind, bleibt nur noch eine kurze Endsumme.

Schritt 4: Den letzten Teilblock korrigieren

Falls \(s=0\), ist der normierte Kernbeitrag einfach

$$\gamma(n,k,r)=a_b.$$

Falls \(s>0\), braucht man zusätzlich die verschobene Reihe

$$G_{k,s}(z)=\sum_{j\ge 0}(-1)^j u_{s+jk}z^j.$$

Der gesuchte Kern ist dann der Koeffizient von \(z^b\) im Produkt \(A_k(z)G_{k,s}(z)\):

$$\gamma(n,k,r)=[z^b](A_k(z)G_{k,s}(z))=\sum_{j=0}^{b}(-1)^j a_{b-j}u_{s+jk}.$$

Damit verarbeitet dieselbe Rekursion alle vollständigen \(k\)-Blöcke, und die verschobene Restreihe erfasst genau die letzten \(s\) Elemente.

Schritt 5: \(P(k,r,n)\) zurückgewinnen und \(Q(n)\) zusammensetzen

Nachdem der normierte Kern berechnet ist, lautet die gesuchte Anzahl

$$P(k,r,n)=(n!)^r\gamma(n,k,r)\pmod{M}.$$

Für das Project-Euler-Ziel setzen die Implementierungen \(r=n\) und summieren über alle \(k\):

$$Q(n)=(n!)^n\sum_{k=1}^{n}\gamma(n,k,n)\pmod{M}.$$

Das gesamte Problem reduziert sich also auf einen reziproken Reihenkoeffizienten pro \(k\), danach auf eine einzige modulare Gesamtsumme.

Durchgerechnetes Beispiel: \(P(1,2,3)=19\)

Nimm \(k=1\), \(r=2\) und \(n=3\). Dann gilt \(b=3\) und \(s=0\). Die relevanten Gewichte sind

$$u_1=\frac{1}{(1!)^2}=1,\qquad u_2=\frac{1}{(2!)^2}=\frac14,\qquad u_3=\frac{1}{(3!)^2}=\frac1{36}.$$

Nun wendet man die Rekursion an:

$$a_0=1,$$

$$a_1=a_0u_1=1,$$

$$a_2=a_1u_1-a_0u_2=1-\frac14=\frac34,$$

$$a_3=a_2u_1-a_1u_2+a_0u_3=\frac34-\frac14+\frac1{36}=\frac{19}{36}.$$

Wegen \(s=0\) ist \(\gamma(3,1,2)=a_3\). Daher

$$P(1,2,3)=(3!)^2\cdot \frac{19}{36}=36\cdot \frac{19}{36}=19,$$

genau der eingebaute Kontrollwert der Implementierungen.

Wie der Code arbeitet

Die C++, Python- und Java-Implementierungen folgen derselben Mathematik. Zuerst werden Fakultäten und inverse Fakultäten vorab berechnet. Danach wird jede inverse Fakultät auf den Exponenten \(r\) potenziert, um die Tabelle \(u_m\) zu erhalten. Für festes \(k\) füllen die Programme die Koeffizientenfolge \(a_0,a_1,\dots,a_b\) mit der alternierenden Faltung und werten bei \(s>0\) zusätzlich die verschobene Endsumme aus.

Für \(P(k,r,n)\) multipliziert die Implementierung den normierten Kern mit \((n!)^r\). Für \(Q(n)\) wird \(r=n\) verwendet, derselbe Kern für jedes \(1\le k\le n\) berechnet, alles aufsummiert und erst dann mit \((n!)^n\) multipliziert. Die C++-Version parallelisiert die äußere Schleife über \(k\), die Java-Version führt dieselbe Logik seriell aus, und die Python-Version fungiert als schlanke Brücke zum kompilierten C++-Löser.

Komplexitätsanalyse

Das Vorberechnen der Fakultäten und inversen Fakultäten kostet \(O(n)\). Das Aufbauen der Tabelle \(u_m=(m!)^{-r}\) per schneller modularer Potenzierung kostet \(O(n\log r)\). Für festes \(k\) hat die Rekursion die Länge \(b=\lfloor n/k\rfloor\), und die verschachtelte alternierende Summe benötigt \(O(b^2)\) Zeit. Deshalb gilt

$$\sum_{k=1}^{n} O\!\left(\left\lfloor\frac{n}{k}\right\rfloor^2\right)=O(n^2),$$

sodass \(Q(n)\) insgesamt in \(O(n^2)\) Zeit berechnet wird. Die Haupttabellen brauchen \(O(n)\) Speicher; die mehrfädige C++-Version ergänzt pro Worker-Thread ein wiederverwendbares Arbeitsfeld.

Fußnoten und Quellen

  1. Problemseite: https://projecteuler.net/problem=559
  2. Permutation: Wikipedia — Permutation
  3. Inklusions-Exklusions-Prinzip: Wikipedia — Inclusion-exclusion principle
  4. Formale Potenzreihe: Wikipedia — Formal power series
  5. Multiplikatives Inverses modulo \(M\): Wikipedia — Modular multiplicative inverse

Problem Özeti

Problem 559,

$$M=1000000123$$

asal modülü altında \(Q(50000)\) değerini ister. Asıl matris sayımı, yardımcı \(P(k,r,n)\) nicelikleri üzerinden kodlanır. C++, Python ve Java uygulamaları matrisleri doğrudan üretmez; bunun yerine sayımı, ters faktöriyel kuvvetlerinden oluşan ağırlıklar, dönüşümlü işaretler ve \(k\) üzerinde tek bir dış toplam ile kurulan bir katsayı çıkarma problemine dönüştürür.

Matematiksel Yaklaşım

\(n\), \(r\) ve \(k\) sabit olsun. Uygulamalar önce normalize edilmiş bir çekirdek değer hesaplar, \((n!)^r\) çarpanı ise en sonda geri eklenir. Bu normalizasyon, kombinatorik yapıyı açık bir ters-seri bağıntısına indirger.

Adım 1: Faktöriyel kuvvetleriyle normalize et

\(M\) asal olduğu ve hesaplama yalnızca \(n=50000\lt M\) için gereken faktöriyellere kadar çıktığı için her \(m!\) değeri modulo \(M\) içinde terslenebilir. Şunu tanımlayalım:

$$u_m=(m!)^{-r}\pmod{M}\qquad (0\le m\le n).$$

Bu sayılar yöntemin temel ağırlıklarıdır. Faktöriyelle bölme işlemleri modüler terslerle temsil edilir; eksik kalan \((n!)^r\) çarpanı ise normalize katsayı bulunduktan sonra geri konur.

Adım 2: \(n\)'yi tam \(k\)-bloklarına ve bir kalana ayır

Şöyle yazalım:

$$n=bk+s,\qquad b=\left\lfloor\frac{n}{k}\right\rfloor,\qquad 0\le s\lt k.$$

Bağıntı yalnızca \(k\) büyüklüğünde sıçramalarla ilerler. Bu yüzden \(b\), kaç tam \(k\)-adımı olduğunu; \(s\) ise en sondaki eksik parçayı gösterir. Formüllerin bir tam-blok kısmı ve bir artık düzeltmesi olarak ayrılması tam olarak bundan kaynaklanır.

Adım 3: Ters formal kuvvet serisini kur

Önce şu formal kuvvet serisini tanımlayalım:

$$F_k(z)=\sum_{j\ge 0}(-1)^j u_{jk}z^j=1-u_k z+u_{2k}z^2-u_{3k}z^3+\cdots.$$

Şimdi bunun tersini alalım:

$$A_k(z)=\frac{1}{F_k(z)}=\sum_{i\ge 0} a_i z^i.$$

\(A_k(z)F_k(z)=1\) eşitliğinde katsayıları karşılaştırınca

$$a_0=1,$$

$$a_i=\sum_{j=1}^{i}(-1)^{j+1}a_{i-j}u_{jk}\qquad (i\ge 1)$$

elde edilir. Algoritmanın kalbi bu dönüşümlü evrişimdir. \(a_0,a_1,\dots,a_b\) dizisi hesaplanınca geriye yalnızca kısa bir bitiş toplamı kalır.

Adım 4: Son eksik bloğu düzelt

Eğer \(s=0\) ise normalize çekirdek doğrudan

$$\gamma(n,k,r)=a_b$$

olur. Eğer \(s>0\) ise bir de kaydırılmış seri gerekir:

$$G_{k,s}(z)=\sum_{j\ge 0}(-1)^j u_{s+jk}z^j.$$

İstenen çekirdek, \(A_k(z)G_{k,s}(z)\) çarpımındaki \(z^b\) katsayısıdır:

$$\gamma(n,k,r)=[z^b](A_k(z)G_{k,s}(z))=\sum_{j=0}^{b}(-1)^j a_{b-j}u_{s+jk}.$$

Böylece aynı bağıntı tüm tam \(k\)-bloklarını işler; kaydırılmış son seri de geride kalan \(s\) elemanlık parçayı hesaba katar.

Adım 5: \(P(k,r,n)\)'yi geri kazan ve \(Q(n)\)'yi kur

Normalize çekirdek hesaplandıktan sonra istenen sayı

$$P(k,r,n)=(n!)^r\gamma(n,k,r)\pmod{M}$$

şeklindedir. Project Euler hedefinde uygulamalar \(r=n\) alır ve tüm \(k\) değerleri üzerinde toplar:

$$Q(n)=(n!)^n\sum_{k=1}^{n}\gamma(n,k,n)\pmod{M}.$$

Dolayısıyla tüm problem, her \(k\) için bir ters-seri katsayısı hesaplamaya, bu normalize çekirdekleri toplamaya ve sonda tek bir çarpım yapmaya indirgenir.

Çözümlü Örnek: \(P(1,2,3)=19\)

\(k=1\), \(r=2\), \(n=3\) alalım. Bu durumda \(b=3\) ve \(s=0\). Gerekli ağırlıklar

$$u_1=\frac{1}{(1!)^2}=1,\qquad u_2=\frac{1}{(2!)^2}=\frac14,\qquad u_3=\frac{1}{(3!)^2}=\frac1{36}$$

olur. Şimdi bağıntıyı uygularız:

$$a_0=1,$$

$$a_1=a_0u_1=1,$$

$$a_2=a_1u_1-a_0u_2=1-\frac14=\frac34,$$

$$a_3=a_2u_1-a_1u_2+a_0u_3=\frac34-\frac14+\frac1{36}=\frac{19}{36}.$$

\(s=0\) olduğu için \(\gamma(3,1,2)=a_3\) olur. O halde

$$P(1,2,3)=(3!)^2\cdot \frac{19}{36}=36\cdot \frac{19}{36}=19,$$

ve bu, uygulamalardaki yerleşik kontrol değeriyle aynıdır.

Kod Nasıl Çalışır

C++, Python ve Java uygulamaları aynı matematiği izler. Önce faktöriyeller ve modüler ters faktöriyeller önceden hazırlanır. Ardından her ters faktöriyel, gerekli \(r\) üssüne yükseltilerek \(u_m\) tablosu üretilir. Sabit bir \(k\) için programlar yukarıdaki dönüşümlü evrişimle \(a_0,a_1,\dots,a_b\) dizisini doldurur ve \(s>0\) ise kaydırılmış bitiş toplamını hesaplar.

\(P(k,r,n)\) için uygulama normalize çekirdeği \((n!)^r\) ile çarpar. \(Q(n)\) için \(r=n\) alınır, aynı çekirdek hesabı her \(1\le k\le n\) için tekrarlanır, çekirdekler toplanır ve en sonda \((n!)^n\) ile çarpılır. C++ sürümü dıştaki \(k\) döngüsünü paralelleştirir, Java sürümü aynı mantığı tek iş parçacığında çalıştırır, Python sürümü ise derlenmiş C++ çözücüsünü çağıran ince bir köprü görevi görür.

Karmaşıklık Analizi

Faktöriyel ve ters faktöriyel tablolarını hazırlamak \(O(n)\) zaman alır. \(u_m=(m!)^{-r}\) tablosunu tekrarlı hızlı üs alma ile kurmak \(O(n\log r)\) maliyetlidir. Sabit bir \(k\) için bağıntı uzunluğu \(b=\lfloor n/k\rfloor\) olur ve iç içe dönüşümlü toplam \(O(b^2)\) zaman ister. Bu nedenle

$$\sum_{k=1}^{n} O\!\left(\left\lfloor\frac{n}{k}\right\rfloor^2\right)=O(n^2)$$

elde edilir; yani \(Q(n)\) toplamda \(O(n^2)\) zamanda hesaplanır. Ana tablolar \(O(n)\) bellek kullanır; çok iş parçacıklı C++ sürümü buna ek olarak her işçi için yeniden kullanılan bir çalışma dizisi tutar.

Dipnotlar ve Kaynaklar

  1. Problem sayfası: https://projecteuler.net/problem=559
  2. Permütasyon: Wikipedia — Permutation
  3. Dahil et-çıkar ilkesi: Wikipedia — Inclusion-exclusion principle
  4. Formal kuvvet serileri: Wikipedia — Formal power series
  5. Modüler çarpmaya göre ters: Wikipedia — Modular multiplicative inverse

Resumen del Problema

El problema 559 pide el valor de \(Q(50000)\) módulo el primo

$$M=1000000123.$$

La formulación de conteo de matrices se codifica mediante las cantidades auxiliares \(P(k,r,n)\). Las implementaciones en C++, Python y Java no intentan enumerar matrices de forma directa; transforman el conteo en un problema de extracción de coeficientes con potencias de factoriales inversos, signos alternantes y una suma exterior sobre \(k\).

Enfoque Matemático

Fijemos \(n\), \(r\) y \(k\). Primero se calcula un núcleo normalizado y solo al final se vuelve a multiplicar por \((n!)^r\). Esa normalización convierte el problema combinatorio en una recurrencia limpia de series recíprocas.

Paso 1: Normalizar con potencias factoriales

Como \(M\) es primo y la computación solo necesita factoriales hasta \(n=50000\lt M\), cada \(m!\) es invertible módulo \(M\). Definimos

$$u_m=(m!)^{-r}\pmod{M}\qquad (0\le m\le n).$$

Estos valores son los pesos básicos del método. Toda división por factoriales se reemplaza por inversos modulares, y el factor faltante \((n!)^r\) se restaura únicamente después de obtener el coeficiente normalizado.

Paso 2: Separar \(n\) en bloques completos de tamaño \(k\) y un resto

Escribimos

$$n=bk+s,\qquad b=\left\lfloor\frac{n}{k}\right\rfloor,\qquad 0\le s\lt k.$$

La recurrencia avanza en saltos de tamaño \(k\). El cociente \(b\) cuenta cuántos pasos completos caben en \(n\), mientras que el resto \(s\) registra la parte final incompleta. Por eso las fórmulas se dividen de manera natural en una parte de bloques completos y una corrección final.

Paso 3: Construir la serie recíproca

Introducimos la serie formal

$$F_k(z)=\sum_{j\ge 0}(-1)^j u_{jk}z^j=1-u_k z+u_{2k}z^2-u_{3k}z^3+\cdots.$$

Ahora definimos su recíproca

$$A_k(z)=\frac{1}{F_k(z)}=\sum_{i\ge 0} a_i z^i.$$

Al comparar coeficientes en \(A_k(z)F_k(z)=1\), obtenemos

$$a_0=1,$$

$$a_i=\sum_{j=1}^{i}(-1)^{j+1}a_{i-j}u_{jk}\qquad (i\ge 1).$$

Esta convolución alternante es el núcleo del algoritmo. Una vez conocida la sucesión \(a_0,a_1,\dots,a_b\), solo queda una suma final corta.

Paso 4: Corregir el bloque parcial final

Si \(s=0\), el núcleo normalizado es simplemente

$$\gamma(n,k,r)=a_b.$$

Si \(s>0\), hace falta una serie desplazada adicional:

$$G_{k,s}(z)=\sum_{j\ge 0}(-1)^j u_{s+jk}z^j.$$

Entonces el núcleo buscado es el coeficiente de \(z^b\) en el producto \(A_k(z)G_{k,s}(z)\):

$$\gamma(n,k,r)=[z^b](A_k(z)G_{k,s}(z))=\sum_{j=0}^{b}(-1)^j a_{b-j}u_{s+jk}.$$

Así, la misma recurrencia maneja todos los bloques completos de tamaño \(k\), y la serie desplazada corrige exactamente las \(s\) posiciones sobrantes.

Paso 5: Recuperar \(P(k,r,n)\) y formar \(Q(n)\)

Una vez calculado el núcleo normalizado, la cantidad pedida es

$$P(k,r,n)=(n!)^r\gamma(n,k,r)\pmod{M}.$$

Para el objetivo de Project Euler, las implementaciones toman \(r=n\) y suman sobre todos los \(k\):

$$Q(n)=(n!)^n\sum_{k=1}^{n}\gamma(n,k,n)\pmod{M}.$$

En consecuencia, todo el problema se reduce a evaluar un coeficiente de serie recíproca para cada \(k\), sumar esos núcleos normalizados y multiplicar una sola vez al final.

Ejemplo Resuelto: \(P(1,2,3)=19\)

Tomemos \(k=1\), \(r=2\) y \(n=3\). Entonces \(b=3\) y \(s=0\). Los pesos relevantes son

$$u_1=\frac{1}{(1!)^2}=1,\qquad u_2=\frac{1}{(2!)^2}=\frac14,\qquad u_3=\frac{1}{(3!)^2}=\frac1{36}.$$

Aplicamos ahora la recurrencia:

$$a_0=1,$$

$$a_1=a_0u_1=1,$$

$$a_2=a_1u_1-a_0u_2=1-\frac14=\frac34,$$

$$a_3=a_2u_1-a_1u_2+a_0u_3=\frac34-\frac14+\frac1{36}=\frac{19}{36}.$$

Como \(s=0\), tenemos \(\gamma(3,1,2)=a_3\). Por lo tanto

$$P(1,2,3)=(3!)^2\cdot \frac{19}{36}=36\cdot \frac{19}{36}=19,$$

lo que coincide con el valor de comprobación incorporado en las implementaciones.

Cómo Funciona el Código

Las implementaciones en C++, Python y Java siguen la misma matemática. Primero precalculan factoriales e inversos factoriales modulares. Después elevan cada inverso factorial al exponente \(r\) para construir la tabla \(u_m\). Para un \(k\) fijo, generan la sucesión \(a_0,a_1,\dots,a_b\) mediante la convolución alternante anterior y, si \(s>0\), evalúan la suma final desplazada.

Para calcular \(P(k,r,n)\), la implementación multiplica el núcleo normalizado por \((n!)^r\). Para calcular \(Q(n)\), usa \(r=n\), repite el mismo cálculo del núcleo para cada \(1\le k\le n\), suma todos los núcleos y solo entonces multiplica por \((n!)^n\). La versión en C++ paraleliza el bucle exterior sobre \(k\), la versión en Java ejecuta la misma lógica en serie y la versión en Python actúa como un puente ligero que invoca el solucionador compilado en C++ y devuelve el resultado numérico.

Análisis de Complejidad

Precalcular factoriales e inversos factoriales cuesta \(O(n)\). Construir la tabla \(u_m=(m!)^{-r}\) con exponenciación rápida repetida cuesta \(O(n\log r)\). Para un \(k\) fijo, la longitud de la recurrencia es \(b=\lfloor n/k\rfloor\), y la suma alternante anidada requiere \(O(b^2)\) tiempo. Por tanto,

$$\sum_{k=1}^{n} O\!\left(\left\lfloor\frac{n}{k}\right\rfloor^2\right)=O(n^2),$$

de modo que \(Q(n)\) se obtiene en \(O(n^2)\) tiempo total. Las tablas principales usan \(O(n)\) memoria; la versión multihilo en C++ añade un arreglo de trabajo reutilizable por cada hilo.

Notas y Referencias

  1. Página del problema: https://projecteuler.net/problem=559
  2. Permutación: Wikipedia — Permutation
  3. Principio de inclusión-exclusión: Wikipedia — Inclusion-exclusion principle
  4. Serie formal de potencias: Wikipedia — Formal power series
  5. Inverso multiplicativo modular: Wikipedia — Modular multiplicative inverse

问题概述

第 559 题要求计算

$$Q(50000)\pmod{M},\qquad M=1000000123,$$

其中 \(M\) 是素数。题目的矩阵计数被编码为辅助量 \(P(k,r,n)\)。C++、Python 和 Java 实现都不直接枚举矩阵,而是把问题改写成一个系数提取问题:核心对象是逆阶乘幂、交替符号的卷积,以及对所有 \(k\) 的一次外层求和。

数学方法

固定 \(n\)、\(r\) 和 \(k\)。实现先求一个“归一化核心值”,最后再乘回 \((n!)^r\)。这样做以后,中间式子会变得非常整齐,本质上就是一个倒数形式幂级数的系数递推。

步骤 1:用阶乘幂做归一化

因为 \(M\) 是素数,而且实际计算只需要到 \(n=50000\lt M\) 为止的阶乘,所以每个 \(m!\) 在模 \(M\) 下都有乘法逆元。定义

$$u_m=(m!)^{-r}\pmod{M}\qquad (0\le m\le n).$$

这些 \(u_m\) 就是整个算法的基本权重。所有“除以阶乘”的操作都被替换成模逆,等到归一化系数算完以后,再统一乘上 \((n!)^r\) 还原成题目需要的计数。

步骤 2:把 \(n\) 分成完整的 \(k\) 块和一个余数块

写成

$$n=bk+s,\qquad b=\left\lfloor\frac{n}{k}\right\rfloor,\qquad 0\le s\lt k.$$

递推每次只按大小为 \(k\) 的步长前进,因此 \(b\) 表示能放下多少个完整的 \(k\) 块,而 \(s\) 记录最后剩下的不完整部分。这也是为什么公式会自然拆成“完整块部分”和“尾部修正部分”。

步骤 3:构造倒数形式的幂级数

先定义形式幂级数

$$F_k(z)=\sum_{j\ge 0}(-1)^j u_{jk}z^j=1-u_k z+u_{2k}z^2-u_{3k}z^3+\cdots.$$

再取它的倒数:

$$A_k(z)=\frac{1}{F_k(z)}=\sum_{i\ge 0} a_i z^i.$$

把 \(A_k(z)F_k(z)=1\) 的同次项系数逐项比较,可得

$$a_0=1,$$

$$a_i=\sum_{j=1}^{i}(-1)^{j+1}a_{i-j}u_{jk}\qquad (i\ge 1).$$

这条交替卷积递推正是实现真正执行的核心步骤。只要把 \(a_0,a_1,\dots,a_b\) 依次求出,后面的工作就只是一个较短的收尾求和。

步骤 4:处理最后的不完整部分

如果 \(s=0\),那么归一化核心值直接就是

$$\gamma(n,k,r)=a_b.$$

如果 \(s>0\),则还需要一个“平移后”的级数:

$$G_{k,s}(z)=\sum_{j\ge 0}(-1)^j u_{s+jk}z^j.$$

这时目标核心值就是乘积 \(A_k(z)G_{k,s}(z)\) 中 \(z^b\) 的系数:

$$\gamma(n,k,r)=[z^b](A_k(z)G_{k,s}(z))=\sum_{j=0}^{b}(-1)^j a_{b-j}u_{s+jk}.$$

也就是说,完整的 \(k\) 块仍然由同一条递推处理,而最后剩下的 \(s\) 个位置通过这个平移级数被精确修正进去。

步骤 5:恢复 \(P(k,r,n)\),再汇总成 \(Q(n)\)

当归一化核心值求出以后,真正需要的计数是

$$P(k,r,n)=(n!)^r\gamma(n,k,r)\pmod{M}.$$

对于本题目标,三种实现都取 \(r=n\),并对所有 \(k\) 求和:

$$Q(n)=(n!)^n\sum_{k=1}^{n}\gamma(n,k,n)\pmod{M}.$$

因此整个问题可以概括成三步:对每个 \(k\) 计算一个倒数幂级数系数,累加这些归一化核心值,最后乘上统一的 \((n!)^n\) 因子。

完整示例:\(P(1,2,3)=19\)

取 \(k=1\)、\(r=2\)、\(n=3\)。此时 \(b=3\)、\(s=0\)。相关权重为

$$u_1=\frac{1}{(1!)^2}=1,\qquad u_2=\frac{1}{(2!)^2}=\frac14,\qquad u_3=\frac{1}{(3!)^2}=\frac1{36}.$$

按照递推逐步计算:

$$a_0=1,$$

$$a_1=a_0u_1=1,$$

$$a_2=a_1u_1-a_0u_2=1-\frac14=\frac34,$$

$$a_3=a_2u_1-a_1u_2+a_0u_3=\frac34-\frac14+\frac1{36}=\frac{19}{36}.$$

由于 \(s=0\),所以 \(\gamma(3,1,2)=a_3\)。于是

$$P(1,2,3)=(3!)^2\cdot \frac{19}{36}=36\cdot \frac{19}{36}=19,$$

这与实现内置的校验值完全一致。这个例子也说明了为什么先做归一化会更方便:中间可以用非常短的递推式,最后再把 \((n!)^r\) 一次性乘回去。

代码如何工作

C++、Python 和 Java 实现遵循完全相同的数学结构。第一步是预计算阶乘与模逆阶乘。第二步是把每个逆阶乘提升到所需指数 \(r\),得到权重表 \(u_m\)。接下来对固定的 \(k\),用上面的交替卷积递推填出 \(a_0,a_1,\dots,a_b\),如果 \(s>0\),再额外计算一次平移后的收尾求和。

在求 \(P(k,r,n)\) 时,实现会把归一化核心值乘上 \((n!)^r\)。在求 \(Q(n)\) 时,它们把 \(r\) 设成 \(n\),对每个 \(1\le k\le n\) 重复同样的核心计算,把所有核心值先加起来,最后才乘上 \((n!)^n\)。其中 C++ 版本把外层的 \(k\) 循环并行化;Java 版本以单线程方式执行同样的流程;Python 版本则不重复实现整套递推,而是负责调用编译后的 C++ 求解器并返回最终数值。

复杂度分析

预计算阶乘和逆阶乘需要 \(O(n)\) 时间。利用快速幂构造 \(u_m=(m!)^{-r}\) 表需要 \(O(n\log r)\) 时间。对固定的 \(k\),递推长度是 \(b=\lfloor n/k\rfloor\),嵌套的交替求和代价是 \(O(b^2)\)。因此总时间满足

$$\sum_{k=1}^{n} O\!\left(\left\lfloor\frac{n}{k}\right\rfloor^2\right)=O(n^2).$$

也就是说,计算 \(Q(n)\) 的总体时间复杂度为 \(O(n^2)\)。主要表格占用 \(O(n)\) 内存;多线程 C++ 版本还会为每个工作线程保留一个可复用的工作数组。

注释与参考资料

  1. 题目页面:https://projecteuler.net/problem=559
  2. 置换:Wikipedia — Permutation
  3. 容斥原理:Wikipedia — Inclusion-exclusion principle
  4. 形式幂级数:Wikipedia — Formal power series
  5. 模乘法逆元:Wikipedia — Modular multiplicative inverse

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

Задача 559 требует вычислить \(Q(50000)\) по модулю простого числа

$$M=1000000123.$$

Подсчёт матриц переписывается через вспомогательные величины \(P(k,r,n)\). Реализации на C++, Python и Java не перебирают матрицы напрямую. Вместо этого они сводят задачу к извлечению коэффициента из ряда, построенного из степеней обратных факториалов, чередующихся знаков и внешней суммы по всем \(k\).

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

Зафиксируем \(n\), \(r\) и \(k\). Сначала вычисляется нормированное ядро, а множитель \((n!)^r\) возвращается только в самом конце. Благодаря этому комбинаторная структура превращается в прозрачную рекурсию для обратного степенного ряда.

Шаг 1: Нормировка степенями факториалов

Так как \(M\) простое и в вычислении нужны только факториалы до \(n=50000\lt M\), каждый \(m!\) обратим по модулю \(M\). Введём

$$u_m=(m!)^{-r}\pmod{M}\qquad (0\le m\le n).$$

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

Шаг 2: Разбиение \(n\) на полные блоки размера \(k\) и остаток

Запишем

$$n=bk+s,\qquad b=\left\lfloor\frac{n}{k}\right\rfloor,\qquad 0\le s\lt k.$$

Рекурсия движется шагами длины \(k\). Число \(b\) показывает, сколько полных \(k\)-блоков помещается в \(n\), а остаток \(s\) отвечает за финальную неполную часть. Поэтому формулы естественно распадаются на полноблочную часть и отдельную поправку для хвоста.

Шаг 3: Построение обратного степенного ряда

Введём формальный степенной ряд

$$F_k(z)=\sum_{j\ge 0}(-1)^j u_{jk}z^j=1-u_k z+u_{2k}z^2-u_{3k}z^3+\cdots.$$

Теперь рассмотрим его обратный ряд

$$A_k(z)=\frac{1}{F_k(z)}=\sum_{i\ge 0} a_i z^i.$$

Сравнение коэффициентов в равенстве \(A_k(z)F_k(z)=1\) даёт

$$a_0=1,$$

$$a_i=\sum_{j=1}^{i}(-1)^{j+1}a_{i-j}u_{jk}\qquad (i\ge 1).$$

Эта знакопеременная свёртка и есть центральная часть алгоритма. После того как найдены \(a_0,a_1,\dots,a_b\), остаётся только короткая завершающая сумма.

Шаг 4: Учёт последнего неполного блока

Если \(s=0\), нормированное ядро равно

$$\gamma(n,k,r)=a_b.$$

Если \(s>0\), требуется ещё один сдвинутый ряд:

$$G_{k,s}(z)=\sum_{j\ge 0}(-1)^j u_{s+jk}z^j.$$

Тогда нужное ядро есть коэффициент при \(z^b\) в произведении \(A_k(z)G_{k,s}(z)\):

$$\gamma(n,k,r)=[z^b](A_k(z)G_{k,s}(z))=\sum_{j=0}^{b}(-1)^j a_{b-j}u_{s+jk}.$$

Тем самым одна и та же рекурсия обрабатывает все полные \(k\)-блоки, а сдвинутый хвост точно учитывает оставшиеся \(s\) позиций.

Шаг 5: Восстановление \(P(k,r,n)\) и построение \(Q(n)\)

После вычисления нормированного ядра искомое количество равно

$$P(k,r,n)=(n!)^r\gamma(n,k,r)\pmod{M}.$$

Для целевой задачи реализации берут \(r=n\) и суммируют по всем \(k\):

$$Q(n)=(n!)^n\sum_{k=1}^{n}\gamma(n,k,n)\pmod{M}.$$

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

Разобранный пример: \(P(1,2,3)=19\)

Возьмём \(k=1\), \(r=2\), \(n=3\). Тогда \(b=3\) и \(s=0\). Нужные веса:

$$u_1=\frac{1}{(1!)^2}=1,\qquad u_2=\frac{1}{(2!)^2}=\frac14,\qquad u_3=\frac{1}{(3!)^2}=\frac1{36}.$$

Применяем рекурсию:

$$a_0=1,$$

$$a_1=a_0u_1=1,$$

$$a_2=a_1u_1-a_0u_2=1-\frac14=\frac34,$$

$$a_3=a_2u_1-a_1u_2+a_0u_3=\frac34-\frac14+\frac1{36}=\frac{19}{36}.$$

Так как \(s=0\), получаем \(\gamma(3,1,2)=a_3\). Следовательно,

$$P(1,2,3)=(3!)^2\cdot \frac{19}{36}=36\cdot \frac{19}{36}=19,$$

что совпадает со встроенной проверкой в реализациях.

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

Реализации на C++, Python и Java используют одну и ту же математическую схему. Сначала они заранее вычисляют факториалы и обратные факториалы по модулю. Затем каждый обратный факториал возводится в степень \(r\), чтобы получить таблицу \(u_m\). Для фиксированного \(k\) программы строят последовательность \(a_0,a_1,\dots,a_b\) по знакопеременной свёртке, а при \(s>0\) дополнительно вычисляют сдвинутую завершающую сумму.

Чтобы найти \(P(k,r,n)\), реализация умножает нормированное ядро на \((n!)^r\). Чтобы найти \(Q(n)\), берётся \(r=n\), один и тот же расчёт ядра повторяется для каждого \(1\le k\le n\), все ядра суммируются, и только потом результат умножается на \((n!)^n\). Версия на C++ распараллеливает внешний цикл по \(k\), версия на Java выполняет ту же логику последовательно, а версия на Python служит тонким мостом: она запускает скомпилированный решатель на C++ и возвращает итоговое число.

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

Предварительное вычисление факториалов и обратных факториалов занимает \(O(n)\). Построение таблицы \(u_m=(m!)^{-r}\) с помощью повторной быстрой экспоненты занимает \(O(n\log r)\). Для фиксированного \(k\) длина рекурсии равна \(b=\lfloor n/k\rfloor\), а вложенная знакопеременная сумма стоит \(O(b^2)\) времени. Поэтому

$$\sum_{k=1}^{n} O\!\left(\left\lfloor\frac{n}{k}\right\rfloor^2\right)=O(n^2),$$

и общая трудоёмкость вычисления \(Q(n)\) составляет \(O(n^2)\). Основные таблицы требуют \(O(n)\) памяти; многопоточная версия на C++ добавляет по одному переиспользуемому рабочему массиву на поток.

Сноски и ссылки

  1. Страница задачи: https://projecteuler.net/problem=559
  2. Перестановка: Wikipedia — Permutation
  3. Принцип включения-исключения: Wikipedia — Inclusion-exclusion principle
  4. Формальные степенные ряды: Wikipedia — Formal power series
  5. Модульный обратный элемент: Wikipedia — Modular multiplicative inverse

ملخص المسألة

المطلوب في المسألة 559 هو حساب \(Q(50000)\) بترديد العدد الأولي

$$M=1000000123.$$

صياغة عدّ المصفوفات تُكتب عبر الكميات المساعدة \(P(k,r,n)\). لا تحاول تطبيقات ++C وPython وJava تعداد المصفوفات مباشرة، بل تحوّل العد إلى مسألة استخراج معامل من متسلسلة مبنية على قوى مقلوبات المضروب، مع إشارات متناوبة ومجموع خارجي على جميع قيم \(k\).

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

لنثبّت \(n\) و\(r\) و\(k\). تحسب التطبيقات أولاً نواة مُطبَّعة، ثم تعيد ضرب النتيجة في \((n!)^r\) في النهاية فقط. هذا التطبيع يجعل البنية التوافقية تظهر على شكل علاقة تكرارية واضحة لسلسلة مقلوبة.

الخطوة 1: التطبيع بقوى المضروب

بما أن \(M\) أولي، ولأن الحساب لا يحتاج إلا إلى مضاريب حتى \(n=50000\lt M\)، فإن كل \(m!\) قابل للعكس بترديد \(M\). نعرّف

$$u_m=(m!)^{-r}\pmod{M}\qquad (0\le m\le n).$$

هذه القيم هي الأوزان الأساسية في الطريقة. كل قسمة على مضروب تُستبدل بمعكوس ضربي بترديد \(M\)، ثم يُعاد العامل \((n!)^r\) بعد انتهاء حساب المعامل المُطبَّع.

الخطوة 2: تقسيم \(n\) إلى كتل كاملة من الحجم \(k\) وباقٍ نهائي

نكتب

$$n=bk+s,\qquad b=\left\lfloor\frac{n}{k}\right\rfloor,\qquad 0\le s\lt k.$$

العلاقة التكرارية تتحرك بخطوات طولها \(k\). لذلك فإن \(b\) يساوي عدد الكتل الكاملة، بينما يسجل \(s\) الجزء الأخير غير المكتمل. ولهذا السبب تنقسم الصيغ طبيعياً إلى جزء خاص بالكتل الكاملة وجزء آخر لتصحيح الباقي.

الخطوة 3: بناء السلسلة المقلوبة

لنعرّف السلسلة الشكلية

$$F_k(z)=\sum_{j\ge 0}(-1)^j u_{jk}z^j=1-u_k z+u_{2k}z^2-u_{3k}z^3+\cdots.$$

ثم نأخذ مقلوبها:

$$A_k(z)=\frac{1}{F_k(z)}=\sum_{i\ge 0} a_i z^i.$$

بمقارنة المعاملات في العلاقة \(A_k(z)F_k(z)=1\) نحصل على

$$a_0=1,$$

$$a_i=\sum_{j=1}^{i}(-1)^{j+1}a_{i-j}u_{jk}\qquad (i\ge 1).$$

هذا الالتفاف ذو الإشارات المتناوبة هو قلب الخوارزمية. وما إن تُحسَب القيم \(a_0,a_1,\dots,a_b\)، حتى لا يبقى إلا مجموع ختامي قصير.

الخطوة 4: تصحيح الجزء الأخير غير الكامل

إذا كان \(s=0\)، فإن النواة المُطبَّعة تساوي مباشرة

$$\gamma(n,k,r)=a_b.$$

أما إذا كان \(s>0\)، فنحتاج إلى سلسلة مزاحة إضافية:

$$G_{k,s}(z)=\sum_{j\ge 0}(-1)^j u_{s+jk}z^j.$$

وعندئذ تكون النواة المطلوبة هي معامل \(z^b\) في حاصل الضرب \(A_k(z)G_{k,s}(z)\):

$$\gamma(n,k,r)=[z^b](A_k(z)G_{k,s}(z))=\sum_{j=0}^{b}(-1)^j a_{b-j}u_{s+jk}.$$

وهكذا تعالج العلاقة التكرارية جميع الكتل الكاملة، بينما تُدخِل السلسلة المزاحة أثر العناصر \(s\) المتبقية في النهاية.

الخطوة 5: استرجاع \(P(k,r,n)\) ثم تجميع \(Q(n)\)

بعد حساب النواة المُطبَّعة تصبح الكمية المطلوبة

$$P(k,r,n)=(n!)^r\gamma(n,k,r)\pmod{M}.$$

وفي الهدف الخاص بمسألة Project Euler تأخذ التطبيقات \(r=n\) ثم تجمع على جميع قيم \(k\):

$$Q(n)=(n!)^n\sum_{k=1}^{n}\gamma(n,k,n)\pmod{M}.$$

إذن المسألة كلها تختزل إلى حساب معامل واحد من سلسلة مقلوبة لكل \(k\)، ثم جمع هذه الأنوية المُطبَّعة، ثم إجراء ضرب نهائي واحد.

مثال محلول: \(P(1,2,3)=19\)

خذ \(k=1\) و\(r=2\) و\(n=3\). عندئذٍ \(b=3\) و\(s=0\). الأوزان اللازمة هي

$$u_1=\frac{1}{(1!)^2}=1,\qquad u_2=\frac{1}{(2!)^2}=\frac14,\qquad u_3=\frac{1}{(3!)^2}=\frac1{36}.$$

نطبّق الآن العلاقة التكرارية:

$$a_0=1,$$

$$a_1=a_0u_1=1,$$

$$a_2=a_1u_1-a_0u_2=1-\frac14=\frac34,$$

$$a_3=a_2u_1-a_1u_2+a_0u_3=\frac34-\frac14+\frac1{36}=\frac{19}{36}.$$

وبما أن \(s=0\)، فإن \(\gamma(3,1,2)=a_3\). لذلك

$$P(1,2,3)=(3!)^2\cdot \frac{19}{36}=36\cdot \frac{19}{36}=19,$$

وهو تماماً نفس مقدار التحقق الداخلي المستخدم في التطبيقات.

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

تتبع تطبيقات ++C وPython وJava البنية الرياضية نفسها. تبدأ بحساب المضاريب ومقلوبات المضاريب بترديد \(M\)، ثم ترفع كل مقلوب مضروب إلى الأس \(r\) لبناء الجدول \(u_m\). بعد ذلك، ومن أجل \(k\) ثابت، تُولَّد المتتالية \(a_0,a_1,\dots,a_b\) بواسطة الالتفاف المتناوب السابق، وإذا كان \(s>0\) تُحسب أيضاً النهاية المزاحة.

لحساب \(P(k,r,n)\)، تضرب الشيفرة النواة المُطبَّعة في \((n!)^r\). ولحساب \(Q(n)\)، تضع \(r=n\)، وتكرر الحساب نفسه لكل \(1\le k\le n\)، ثم تجمع جميع الأنوية قبل أن تضرب مرة واحدة في \((n!)^n\). نسخة ++C توازي الحلقة الخارجية على \(k\)، ونسخة Java تنفذ المنطق نفسه على نحو تسلسلي، أما نسخة Python فتعمل كجسر خفيف يستدعي محلل ++C المترجم ويعيد الناتج العددي.

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

يساوي بناء جداول المضروب ومقلوبه \(O(n)\). أما إنشاء الجدول \(u_m=(m!)^{-r}\) باستخدام الأس السريع المتكرر فيكلف \(O(n\log r)\). ولـ \(k\) ثابت، يكون طول العلاقة التكرارية \(b=\lfloor n/k\rfloor\)، بينما تكلف الحلقة المتداخلة ذات الإشارات المتناوبة \(O(b^2)\) زمناً. ومن ثم

$$\sum_{k=1}^{n} O\!\left(\left\lfloor\frac{n}{k}\right\rfloor^2\right)=O(n^2),$$

أي إن حساب \(Q(n)\) كله يتم في \(O(n^2)\) زمنياً. الجداول الرئيسية تستهلك \(O(n)\) من الذاكرة، وتضيف نسخة ++C متعددة الخيوط مصفوفة عمل قابلة لإعادة الاستخدام لكل خيط.

حواشٍ ومراجع

  1. صفحة المسألة: https://projecteuler.net/problem=559
  2. التبديل: Wikipedia — Permutation
  3. مبدأ الشمول والاستبعاد: Wikipedia — Inclusion-exclusion principle
  4. السلاسل الشكلية للقوى: Wikipedia — Formal power series
  5. المعكوس الضربي بترديد \(M\): Wikipedia — Modular multiplicative inverse