Problem Summary

We must evaluate

$$D(m,n)=\sum_{d \mid m}\sum_{k=1}^{n}\sigma_0(kd),$$

for \(m=200!\) and \(n=10^{12}\), modulo \(10^9+7\). Here \(\sigma_0=\tau\) is the divisor-counting function. A direct double loop is hopeless, so the solution rewrites the divisor sum into an inclusion-exclusion over the primes of \(200!\), plus fast queries to the summatory divisor function.

Mathematical Approach

Write \(m=f!\) with \(f=200\), and rename the upper limit as \(N=n\). Let \(\mathbb{P}\) denote the set of prime numbers and define

$$\mathcal{P}=\{p\in\mathbb{P}: p\le f\}.$$

For each \(p\in\mathcal{P}\), the exponent of \(p\) in \(f!\) is

$$e_p=v_p(f!)=\sum_{j\ge 1}\left\lfloor\frac{f}{p^j}\right\rfloor.$$

The implementations exploit this prime-power structure very directly.

Step 1: Factor the divisor sum prime by prime

Every divisor of \(f!\) has the form

$$d=\prod_{p\in\mathcal{P}}p^{a_p},\qquad 0\le a_p\le e_p.$$

For a fixed integer \(k\), write \(b_p=v_p(k)\) for primes \(p\in\mathcal{P}\). Since \(\tau\) is multiplicative on prime powers, the sum over all divisors of \(f!\) separates into local contributions:

$$\sum_{d \mid f!}\tau(kd)=\left(\prod_{q\notin\mathcal{P}}(v_q(k)+1)\right)\prod_{p\in\mathcal{P}}\sum_{a=0}^{e_p}(a+b_p+1).$$

So the entire problem reduces to understanding the one-prime expression \(\sum_{a=0}^{e}(a+b+1)\).

Step 2: Rewrite the local sum with triangular numbers

Define the triangular-number function

$$T(t)=\frac{t(t+1)}{2}.$$

Then

$$\sum_{a=0}^{e}(a+b+1)=(e+1)(b+1)+\frac{e(e+1)}{2}=T(e+1)(b+1)-T(e)b.$$

This identity is the key algebraic step. The term \(T(e+1)(b+1)\) keeps the current exponent of \(p\) inside \(k\), while the correction term \(T(e)b\) corresponds to removing one copy of \(p\) when \(p\mid k\).

Step 3: Expand the product by inclusion-exclusion

Now define

$$K=\prod_{p\in\mathcal{P}}T(e_p+1),\qquad \beta_p=\frac{T(e_p)}{T(e_p+1)}.$$

Choosing the correction term at a prime \(p\) introduces a factor \(-\beta_p\) and can only happen when \(p\) already divides \(k\). If \(Q\subseteq\mathcal{P}\) is the set of primes where we choose that correction, and

$$P_Q=\prod_{p\in Q}p,$$

then one copy of every prime in \(Q\) is removed from \(k\). Therefore, for each fixed \(k\),

$$\sum_{d \mid f!}\tau(kd)=K\sum_{Q\subseteq\mathcal{P},\ P_Q\mid k}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\tau\left(\frac{k}{P_Q}\right).$$

This is exactly the inclusion-exclusion that the implementations realize through a recursive traversal of prime subsets.

Step 4: Swap the order of summation

Now sum over \(1\le k\le N\). For a fixed subset \(Q\), the condition \(P_Q\mid k\) lets us write \(k=P_Qr\), so

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\sum_{r\le N/P_Q}\tau(r).$$

Introduce the summatory divisor function

$$S_\tau(x)=\sum_{r=1}^{x}\tau(r).$$

Then the whole problem becomes

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right).$$

If \(P_Q>N\), the corresponding term is zero, which explains the branch pruning in the recursive search.

Step 5: Evaluate the summatory divisor function quickly

The identity

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

counts factor pairs \((a,b)\) with \(ab\le x\). Splitting those lattice points across the hyperbola \(ab=x\) gives the standard Dirichlet-hyperbola formula

$$S_\tau(x)=2\sum_{d=1}^{\lfloor\sqrt{x}\rfloor}\left\lfloor\frac{x}{d}\right\rfloor-\lfloor\sqrt{x}\rfloor^2.$$

This is the large-\(x\) formula used by the implementation. Small values are handled by a precomputed prefix table.

Worked Example: \(m=3!=6\) and \(N=5\)

Here \(\mathcal{P}=\{2,3\}\) and \(e_2=e_3=1\). Hence

$$T(2)=3,\qquad T(1)=1,\qquad K=3\cdot 3=9,\qquad \beta_2=\beta_3=\frac{1}{3}.$$

The subset formula becomes

$$D(6,5)=9S_\tau(5)-3S_\tau(2)-3S_\tau(1)+S_\tau(0).$$

Now

$$S_\tau(5)=1+2+2+3+2=10,\qquad S_\tau(2)=3,\qquad S_\tau(1)=1,\qquad S_\tau(0)=0,$$

so

$$D(6,5)=9\cdot 10-3\cdot 3-3\cdot 1=78.$$

A direct check agrees:

$$\sum_{k=1}^{5}\tau(k)=10,\qquad \sum_{k=1}^{5}\tau(2k)=17,\qquad \sum_{k=1}^{5}\tau(3k)=19,\qquad \sum_{k=1}^{5}\tau(6k)=32,$$

and therefore

$$10+17+19+32=78.$$

This small case shows exactly how the inclusion-exclusion reproduces the brute-force sum.

How the Code Works

The C++, Python, and Java implementations first enumerate the primes up to \(200\) and compute each factorial exponent \(e_p\) with Legendre's formula. From those exponents they build the global factor \(K\) and the prime-specific ratios \(\beta_p\), always working modulo \(10^9+7\).

Next, they precompute \(S_\tau(x)\) for all \(x<10^6\) with a divisor sieve followed by prefix sums. For larger arguments they use the hyperbola formula above and store the results in a cache, so repeated requests for the same value are answered immediately.

The main recursive traversal walks through the primes in increasing order, maintains the current squarefree product \(P_Q\), the alternating sign, and the multiplicative weight \(K\prod_{p\in Q}\beta_p\). At each visited subset it adds the corresponding term

$$K\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right)$$

with the appropriate sign. Because multiplying by another prime only increases \(P_Q\), the recursion stops as soon as the next product would exceed \(N\).

Complexity Analysis

Let \(B=10^6\). Building the small prefix table costs \(O(B\log B)\) time and \(O(B)\) memory, because every divisor updates all of its multiples. The prime list and factorial exponents up to \(200\) are tiny by comparison.

Let \(R\) be the number of squarefree products of primes \(\le 200\) that do not exceed \(N\). The subset traversal uses \(O(R)\) arithmetic steps. If \(X\) is the set of distinct large arguments passed to \(S_\tau\), then the uncached large-\(x\) cost is

$$O\left(\sum_{x\in X}\sqrt{x}\right),$$

because each such value is computed once with the hyperbola identity and then memoized. In practice, the pruning \(P_Q\le N\) and the cache are what make the computation feasible.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=608
  2. Divisor function: Wikipedia - Divisor function
  3. Divisor summatory function: Wikipedia - Divisor summatory function
  4. Legendre's formula: Wikipedia - Legendre's formula
  5. Inclusion-exclusion principle: Wikipedia - Inclusion-exclusion principle
  6. Dirichlet hyperbola method: Wikipedia - Dirichlet hyperbola method

Problemzusammenfassung

Gesucht ist der Wert von

$$D(m,n)=\sum_{d \mid m}\sum_{k=1}^{n}\sigma_0(kd),$$

für \(m=200!\) und \(n=10^{12}\), modulo \(10^9+7\). Dabei ist \(\sigma_0=\tau\) die Teileranzahlfunktion. Eine direkte doppelte Iteration ist völlig unpraktikabel, daher formt die Lösung die Summe in eine Inklusions-Exklusions-Form über die Primzahlen von \(200!\) plus schnelle Abfragen der summatorischen Teilerfunktion um.

Mathematischer Ansatz

Schreibe \(m=f!\) mit \(f=200\) und nenne die obere Grenze \(N=n\). Sei \(\mathbb{P}\) die Menge aller Primzahlen und setze

$$\mathcal{P}=\{p\in\mathbb{P}: p\le f\}.$$

Für jedes \(p\in\mathcal{P}\) ist der Exponent von \(p\) in \(f!\)

$$e_p=v_p(f!)=\sum_{j\ge 1}\left\lfloor\frac{f}{p^j}\right\rfloor.$$

Genau diese Primzahlpotenzstruktur wird in den Implementierungen ausgenutzt.

Schritt 1: Die Teilersumme primweise zerlegen

Jeder Teiler von \(f!\) hat die Form

$$d=\prod_{p\in\mathcal{P}}p^{a_p},\qquad 0\le a_p\le e_p.$$

Für ein festes \(k\) schreiben wir \(b_p=v_p(k)\) für \(p\in\mathcal{P}\). Weil \(\tau\) auf Primzahlpotenzen multiplikativ ist, zerfällt die Summe über alle Teiler von \(f!\) in lokale Faktoren:

$$\sum_{d \mid f!}\tau(kd)=\left(\prod_{q\notin\mathcal{P}}(v_q(k)+1)\right)\prod_{p\in\mathcal{P}}\sum_{a=0}^{e_p}(a+b_p+1).$$

Damit reduziert sich das Problem auf die lokale Summe \(\sum_{a=0}^{e}(a+b+1)\).

Schritt 2: Die lokale Summe mit Dreieckszahlen umschreiben

Definiere

$$T(t)=\frac{t(t+1)}{2}.$$

Dann gilt

$$\sum_{a=0}^{e}(a+b+1)=(e+1)(b+1)+\frac{e(e+1)}{2}=T(e+1)(b+1)-T(e)b.$$

Das ist der entscheidende algebraische Schritt. Der Term \(T(e+1)(b+1)\) behält den vorhandenen \(p\)-Exponent in \(k\) bei, während der Korrekturterm \(T(e)b\) genau einem Entfernen einer Kopie von \(p\) entspricht, falls \(p\mid k\).

Schritt 3: Das Produkt per Inklusion-Exklusion entwickeln

Setze

$$K=\prod_{p\in\mathcal{P}}T(e_p+1),\qquad \beta_p=\frac{T(e_p)}{T(e_p+1)}.$$

Wählt man an einer Primzahl \(p\) den Korrekturterm, so entsteht ein Faktor \(-\beta_p\), und dies ist nur möglich, wenn \(p\) bereits \(k\) teilt. Ist \(Q\subseteq\mathcal{P}\) die Menge aller so gewählten Primzahlen und

$$P_Q=\prod_{p\in Q}p,$$

dann wird aus \(k\) genau eine Kopie jeder Primzahl aus \(Q\) herausdividiert. Daher gilt für jedes feste \(k\)

$$\sum_{d \mid f!}\tau(kd)=K\sum_{Q\subseteq\mathcal{P},\ P_Q\mid k}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\tau\left(\frac{k}{P_Q}\right).$$

Dies ist exakt die Inklusions-Exklusions-Formel, die die Implementierungen durch eine rekursive Traversierung der Primzahlmengen auswerten.

Schritt 4: Die Summationsreihenfolge vertauschen

Nun summieren wir über \(1\le k\le N\). Für eine feste Menge \(Q\) erlaubt die Bedingung \(P_Q\mid k\) die Substitution \(k=P_Qr\). Damit folgt

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\sum_{r\le N/P_Q}\tau(r).$$

Führe die summatorische Teilerfunktion ein:

$$S_\tau(x)=\sum_{r=1}^{x}\tau(r).$$

Dann wird das Problem zu

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right).$$

Falls \(P_Q>N\), verschwindet der Beitrag. Genau das erklärt das Abschneiden von Zweigen in der rekursiven Suche.

Schritt 5: Die summatorische Teilerfunktion schnell berechnen

Aus dem Zählen von Faktorenpaaren folgt

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

Durch Spiegelung der Gitterpunkte an der Hyperbel \(ab=x\) erhält man die Dirichlet-Hyperbel-Formel

$$S_\tau(x)=2\sum_{d=1}^{\lfloor\sqrt{x}\rfloor}\left\lfloor\frac{x}{d}\right\rfloor-\lfloor\sqrt{x}\rfloor^2.$$

Genau diese Formel verwenden die Implementierungen für große \(x\). Kleine Werte kommen aus einer vorberechneten Präfixtabelle.

Durchgerechnetes Beispiel: \(m=3!=6\) und \(N=5\)

Hier ist \(\mathcal{P}=\{2,3\}\) und \(e_2=e_3=1\). Also

$$T(2)=3,\qquad T(1)=1,\qquad K=3\cdot 3=9,\qquad \beta_2=\beta_3=\frac{1}{3}.$$

Die Teilmengenformel lautet dann

$$D(6,5)=9S_\tau(5)-3S_\tau(2)-3S_\tau(1)+S_\tau(0).$$

Mit

$$S_\tau(5)=10,\qquad S_\tau(2)=3,\qquad S_\tau(1)=1,\qquad S_\tau(0)=0$$

erhält man

$$D(6,5)=9\cdot 10-3\cdot 3-3\cdot 1=78.$$

Eine direkte Kontrolle bestätigt das:

$$\sum_{k=1}^{5}\tau(k)=10,\qquad \sum_{k=1}^{5}\tau(2k)=17,\qquad \sum_{k=1}^{5}\tau(3k)=19,\qquad \sum_{k=1}^{5}\tau(6k)=32,$$

also insgesamt

$$10+17+19+32=78.$$

Das Beispiel zeigt transparent, dass die Inklusions-Exklusions-Formel denselben Wert wie die direkte Auswertung liefert.

Wie der Code funktioniert

Die C++-, Python- und Java-Implementierungen erzeugen zuerst alle Primzahlen bis \(200\) und bestimmen mit der üblichen Fakultäts-Exponentenformel den Wert \(e_p\) für jede dieser Primzahlen. Daraus werden der globale Faktor \(K\) und die primspezifischen Verhältnisse \(\beta_p\) modulo \(10^9+7\) aufgebaut.

Anschließend wird \(S_\tau(x)\) für alle \(x<10^6\) über ein Teilersieb und anschließende Präfixsummen vorbereitet. Für größere Argumente wird die Hyperbel-Formel berechnet und das Ergebnis zwischengespeichert, damit identische Anfragen nicht erneut die Wurzel-Schleife durchlaufen.

Die Hauptrekursion geht die Primzahlen in aufsteigender Reihenfolge durch und hält das aktuelle quadratfreie Produkt \(P_Q\), das Vorzeichen und das Gewicht \(K\prod_{p\in Q}\beta_p\) fest. Für jede besuchte Teilmenge wird der passende Term addiert oder subtrahiert, und sobald das nächste Produkt größer als \(N\) würde, endet der betreffende Zweig sofort.

Komplexitätsanalyse

Sei \(B=10^6\). Das Aufbauen der kleinen Präfixtabelle benötigt \(O(B\log B)\) Zeit und \(O(B)\) Speicher, weil jeder Teiler alle seine Vielfachen aktualisiert. Die Primzahlliste und die Fakultäts-Exponenten bis \(200\) sind dagegen vernachlässigbar klein.

Sei \(R\) die Anzahl quadratfreier Produkte von Primzahlen \(\le 200\), die \(N\) nicht überschreiten. Die Teilmengentraversierung braucht \(O(R)\) arithmetische Schritte. Ist \(X\) die Menge der verschiedenen großen Argumente von \(S_\tau\), so kostet der nicht gecachte Anteil

$$O\left(\sum_{x\in X}\sqrt{x}\right),$$

weil jeder solche Wert genau einmal mit der Hyperbel-Identität berechnet und danach gespeichert wird. Praktisch entscheidend sind also das Abschneiden \(P_Q\le N\) und die Memoisierung.

Fußnoten und Referenzen

  1. Problemseite: https://projecteuler.net/problem=608
  2. Teilerfunktion: Wikipedia - Teilerfunktion
  3. Summatorische Teilerfunktion: Wikipedia - Divisor summatory function
  4. Legendre-Formel: Wikipedia - Legendre-Formel
  5. Inklusions-Exklusions-Prinzip: Wikipedia - Siebformel
  6. Dirichlet-Hyperbelmethode: Wikipedia - Dirichlet hyperbola method

Problem Özeti

Hesaplanması gereken ifade

$$D(m,n)=\sum_{d \mid m}\sum_{k=1}^{n}\sigma_0(kd),$$

ve burada \(m=200!\), \(n=10^{12}\), sonuç da \(10^9+7\) modunda istenir. \(\sigma_0=\tau\) pozitif bölen sayısı fonksiyonudur. Doğrudan tüm \(d\mid 200!\) ve tüm \(k\le n\) değerlerini gezmek mümkün olmadığından çözüm, toplamı \(200!\)'ın asal çarpanları üzerinde bir dahil et-çıkar toplamına ve hızlı bir bölen-toplamı ön-ek fonksiyonuna dönüştürür.

Matematiksel Yaklaşım

\(m=f!\) olacak şekilde \(f=200\) yazalım ve üst sınırı \(N=n\) ile gösterelim. \(\mathbb{P}\) asal sayılar kümesi olsun ve

$$\mathcal{P}=\{p\in\mathbb{P}: p\le f\}.$$

Her \(p\in\mathcal{P}\) için \(f!\) içindeki üs

$$e_p=v_p(f!)=\sum_{j\ge 1}\left\lfloor\frac{f}{p^j}\right\rfloor$$

olur. Uygulamanın kullandığı bütün yapı doğrudan bu asal kuvvet ayrışımından gelir.

Adım 1: Bölen toplamını asallara göre ayır

\(f!\)'ın her böleni

$$d=\prod_{p\in\mathcal{P}}p^{a_p},\qquad 0\le a_p\le e_p$$

şeklindedir. Sabit bir \(k\) için \(p\in\mathcal{P}\) olduğunda \(b_p=v_p(k)\) diyelim. \(\tau\) asal kuvvetlerde çarpımsal davrandığı için, \(f!\)'ın tüm bölenleri üzerindeki toplam yerel çarpanlara ayrılır:

$$\sum_{d \mid f!}\tau(kd)=\left(\prod_{q\notin\mathcal{P}}(v_q(k)+1)\right)\prod_{p\in\mathcal{P}}\sum_{a=0}^{e_p}(a+b_p+1).$$

Dolayısıyla ana problem tek bir asal için çıkan \(\sum_{a=0}^{e}(a+b+1)\) ifadesini anlamaya indirgenir.

Adım 2: Yerel toplamı üçgensel sayılarla yeniden yaz

Üçgensel sayı fonksiyonunu

$$T(t)=\frac{t(t+1)}{2}$$

olarak tanımlayalım. O zaman

$$\sum_{a=0}^{e}(a+b+1)=(e+1)(b+1)+\frac{e(e+1)}{2}=T(e+1)(b+1)-T(e)b.$$

Asıl cebirsel kırılma noktası budur. \(T(e+1)(b+1)\) terimi \(k\) içindeki mevcut \(p\) üssünü korur; \(T(e)b\) düzeltmesi ise \(p\mid k\) olduğunda bir tane \(p\)'yi eksiltmeye karşılık gelir.

Adım 3: Çarpımı dahil et-çıkar ile aç

Şimdi

$$K=\prod_{p\in\mathcal{P}}T(e_p+1),\qquad \beta_p=\frac{T(e_p)}{T(e_p+1)}$$

tanımlarını yapalım. Bir asal \(p\) için düzeltme terimini seçmek \(-\beta_p\) katsayısı getirir ve bu ancak \(p\) zaten \(k\)'yi bölüyorsa mümkündür. Eğer \(Q\subseteq\mathcal{P}\) bu düzeltmenin seçildiği asallar kümesi ise ve

$$P_Q=\prod_{p\in Q}p,$$

o zaman \(Q\) içindeki her asal için \(k\)'den bir kopya çıkarılmış olur. Böylece sabit bir \(k\) için

$$\sum_{d \mid f!}\tau(kd)=K\sum_{Q\subseteq\mathcal{P},\ P_Q\mid k}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\tau\left(\frac{k}{P_Q}\right)$$

elde edilir. Uygulamadaki özyinelemeli asal-altküme dolaşımı tam olarak bu dahil et-çıkar açılımını hesaplar.

Adım 4: Toplama sırasını değiştir

Artık \(1\le k\le N\) üzerinde toplayabiliriz. Sabit bir \(Q\) için \(P_Q\mid k\) koşulu \(k=P_Qr\) yazmamızı sağlar. Böylece

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\sum_{r\le N/P_Q}\tau(r)$$

olur. Şimdi bölen sayısı fonksiyonunun kümülatif toplamını

$$S_\tau(x)=\sum_{r=1}^{x}\tau(r)$$

diye tanımlarsak, sonuç

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right)$$

şeklini alır. \(P_Q>N\) olduğunda katkı sıfırdır; bu yüzden aramadaki dallar erkenden budanabilir.

Adım 5: Kümülatif bölen fonksiyonunu hızlı hesapla

Çarpan çiftlerini sayarak

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

eşitliğini elde ederiz. \(ab=x\) hiperbolünün iki tarafını eşleştirince standart Dirichlet-hiperbol özdeşliği gelir:

$$S_\tau(x)=2\sum_{d=1}^{\lfloor\sqrt{x}\rfloor}\left\lfloor\frac{x}{d}\right\rfloor-\lfloor\sqrt{x}\rfloor^2.$$

Büyük \(x\) için uygulamanın kullandığı formül budur. Küçük değerler ise önceden hazırlanmış bir ön-ek tablosundan okunur.

Çözümlü Örnek: \(m=3!=6\) ve \(N=5\)

Bu durumda \(\mathcal{P}=\{2,3\}\) ve \(e_2=e_3=1\) olur. Dolayısıyla

$$T(2)=3,\qquad T(1)=1,\qquad K=3\cdot 3=9,\qquad \beta_2=\beta_3=\frac{1}{3}.$$

Alt küme formülü

$$D(6,5)=9S_\tau(5)-3S_\tau(2)-3S_\tau(1)+S_\tau(0)$$

haline gelir. Burada

$$S_\tau(5)=10,\qquad S_\tau(2)=3,\qquad S_\tau(1)=1,\qquad S_\tau(0)=0,$$

yani

$$D(6,5)=9\cdot 10-3\cdot 3-3\cdot 1=78.$$

Doğrudan kontrol de aynı sonucu verir:

$$\sum_{k=1}^{5}\tau(k)=10,\qquad \sum_{k=1}^{5}\tau(2k)=17,\qquad \sum_{k=1}^{5}\tau(3k)=19,\qquad \sum_{k=1}^{5}\tau(6k)=32,$$

ve toplam

$$10+17+19+32=78$$

olur. Böylece dahil et-çıkar formülünün küçük bir örnekte kaba kuvvet sonucu aynen verdiği görülür.

Kod Nasıl Çalışır

C++, Python ve Java uygulamaları önce \(200\)'e kadar tüm asalları çıkarır ve her asal için \(200!\) içindeki üs değerini Legendre formülüyle hesaplar. Ardından bu üslerden genel katsayı \(K\) ve asal başına oranlar \(\beta_p\) oluşturulur; bütün aritmetik \(10^9+7\) modunda yapılır.

Sonra \(x<10^6\) için \(S_\tau(x)\) değerleri bir bölen eleği ve ön-ek toplamlarıyla önceden hesaplanır. Daha büyük \(x\) değerleri için hiperbol formülü uygulanır ve sonuçlar önbelleğe alınır; böylece aynı sorgu tekrar geldiğinde karekök döngüsü yeniden çalışmaz.

Ana özyinelemeli dolaşım asalları artan sırayla gezer; mevcut karesiz çarpım \(P_Q\), işaret ve ağırlık \(K\prod_{p\in Q}\beta_p\) birlikte taşınır. Her ziyaret edilen altküme için uygun terim eklenir veya çıkarılır. Bir sonraki asal çarpımı \(N\)'yi aşacaksa dal hemen sonlandırılır.

Karmaşıklık Analizi

\(B=10^6\) olsun. Küçük ön-ek tablosunu kurmak \(O(B\log B)\) zaman ve \(O(B)\) bellek ister; çünkü her bölen bütün katlarını günceller. \(200\)'e kadar asal listesi ve faktöriyel üsleri bunun yanında çok küçüktür.

\(R\), \(200\)'den küçük ya da eşit asalların \(N\)'yi aşmayan karesiz çarpımlarının sayısı olsun. Alt küme dolaşımı \(O(R)\) aritmetik adım kullanır. Eğer \(X\), \(S_\tau\) fonksiyonuna gönderilen farklı büyük argümanların kümesi ise, önbellekte olmayan büyük-\(x\) maliyeti

$$O\left(\sum_{x\in X}\sqrt{x}\right)$$

olur; çünkü her böyle değer hiperbol özdeşliğiyle bir kez hesaplanır ve sonra saklanır. Pratikte hesaplamayı mümkün kılan iki nokta, \(P_Q\le N\) budaması ve önbelleklemedir.

Dipnotlar ve Kaynaklar

  1. Problem sayfası: https://projecteuler.net/problem=608
  2. Bölen fonksiyonu: Wikipedia - Bölen fonksiyonu
  3. Kümülatif bölen fonksiyonu: Wikipedia - Divisor summatory function
  4. Legendre formülü: Wikipedia - Legendre's formula
  5. Dahil et-çıkar ilkesi: Wikipedia - Dahil etme-hariç tutma prensibi
  6. Dirichlet hiperbol yöntemi: Wikipedia - Dirichlet hyperbola method

Resumen del Problema

Hay que calcular

$$D(m,n)=\sum_{d \mid m}\sum_{k=1}^{n}\sigma_0(kd),$$

con \(m=200!\) y \(n=10^{12}\), módulo \(10^9+7\). Aquí \(\sigma_0=\tau\) es la función que cuenta divisores. Un recorrido directo sobre todos los divisores de \(200!\) y todos los \(k\le n\) es inviable, así que la solución transforma la suma doble en una inclusión-exclusión sobre los primos de \(200!\), apoyada por consultas rápidas a la función sumatoria de divisores.

Enfoque Matemático

Escribimos \(m=f!\) con \(f=200\) y denotamos por \(N=n\) el límite superior. Sea \(\mathbb{P}\) el conjunto de los números primos y definamos

$$\mathcal{P}=\{p\in\mathbb{P}: p\le f\}.$$

Para cada \(p\in\mathcal{P}\), el exponente de \(p\) en \(f!\) viene dado por

$$e_p=v_p(f!)=\sum_{j\ge 1}\left\lfloor\frac{f}{p^j}\right\rfloor.$$

Toda la derivación parte de esa descomposición en potencias primas.

Paso 1: Separar la suma por primos

Cada divisor de \(f!\) se escribe como

$$d=\prod_{p\in\mathcal{P}}p^{a_p},\qquad 0\le a_p\le e_p.$$

Fijado un entero \(k\), escribimos \(b_p=v_p(k)\) para \(p\in\mathcal{P}\). Como \(\tau\) es multiplicativa en potencias primas, la suma sobre todos los divisores se factoriza:

$$\sum_{d \mid f!}\tau(kd)=\left(\prod_{q\notin\mathcal{P}}(v_q(k)+1)\right)\prod_{p\in\mathcal{P}}\sum_{a=0}^{e_p}(a+b_p+1).$$

Por tanto, el problema entero se reduce a estudiar la suma local \(\sum_{a=0}^{e}(a+b+1)\).

Paso 2: Reescribir la suma local con números triangulares

Definimos

$$T(t)=\frac{t(t+1)}{2}.$$

Entonces

$$\sum_{a=0}^{e}(a+b+1)=(e+1)(b+1)+\frac{e(e+1)}{2}=T(e+1)(b+1)-T(e)b.$$

Éste es el paso algebraico central. El término \(T(e+1)(b+1)\) conserva el exponente actual de \(p\) dentro de \(k\), mientras que el término correctivo \(T(e)b\) equivale a quitar una copia de \(p\) cuando \(p\mid k\).

Paso 3: Expandir el producto con inclusión-exclusión

Introducimos

$$K=\prod_{p\in\mathcal{P}}T(e_p+1),\qquad \beta_p=\frac{T(e_p)}{T(e_p+1)}.$$

Escoger el término correctivo en un primo \(p\) aporta un factor \(-\beta_p\) y sólo es posible si \(p\) ya divide a \(k\). Si \(Q\subseteq\mathcal{P}\) es el conjunto de primos donde se hace esa elección y

$$P_Q=\prod_{p\in Q}p,$$

entonces se elimina una copia de cada primo de \(Q\) del valor \(k\). Así, para todo \(k\) fijo,

$$\sum_{d \mid f!}\tau(kd)=K\sum_{Q\subseteq\mathcal{P},\ P_Q\mid k}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\tau\left(\frac{k}{P_Q}\right).$$

Ésa es exactamente la identidad que las implementaciones evalúan mediante un recorrido recursivo de subconjuntos de primos.

Paso 4: Intercambiar el orden de las sumas

Ahora sumamos sobre \(1\le k\le N\). Para un subconjunto fijo \(Q\), la condición \(P_Q\mid k\) permite escribir \(k=P_Qr\). Entonces

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\sum_{r\le N/P_Q}\tau(r).$$

Definimos la función sumatoria de divisores

$$S_\tau(x)=\sum_{r=1}^{x}\tau(r).$$

Con ello obtenemos

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right).$$

Si \(P_Q>N\), el término vale cero. Por eso la búsqueda puede podar inmediatamente esas ramas.

Paso 5: Calcular rápido la función sumatoria

Contando pares de factores se obtiene

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

Al emparejar los puntos de la retícula a ambos lados de la hipérbola \(ab=x\), se llega a la identidad clásica de Dirichlet:

$$S_\tau(x)=2\sum_{d=1}^{\lfloor\sqrt{x}\rfloor}\left\lfloor\frac{x}{d}\right\rfloor-\lfloor\sqrt{x}\rfloor^2.$$

Ésta es la fórmula usada para valores grandes de \(x\). Para valores pequeños, la implementación consulta una tabla prefijo precalculada.

Ejemplo Resuelto: \(m=3!=6\) y \(N=5\)

Aquí \(\mathcal{P}=\{2,3\}\) y \(e_2=e_3=1\). Por tanto

$$T(2)=3,\qquad T(1)=1,\qquad K=3\cdot 3=9,\qquad \beta_2=\beta_3=\frac{1}{3}.$$

La fórmula por subconjuntos queda

$$D(6,5)=9S_\tau(5)-3S_\tau(2)-3S_\tau(1)+S_\tau(0).$$

Como

$$S_\tau(5)=10,\qquad S_\tau(2)=3,\qquad S_\tau(1)=1,\qquad S_\tau(0)=0,$$

obtenemos

$$D(6,5)=9\cdot 10-3\cdot 3-3\cdot 1=78.$$

La comprobación directa coincide:

$$\sum_{k=1}^{5}\tau(k)=10,\qquad \sum_{k=1}^{5}\tau(2k)=17,\qquad \sum_{k=1}^{5}\tau(3k)=19,\qquad \sum_{k=1}^{5}\tau(6k)=32,$$

y por tanto

$$10+17+19+32=78.$$

Este ejemplo pequeño muestra con claridad que la fórmula transformada reproduce exactamente la suma ingenua.

Cómo Funciona el Código

Las implementaciones en C++, Python y Java generan primero todos los primos hasta \(200\) y calculan, para cada uno, su exponente en \(200!\) mediante la fórmula de Legendre. A partir de esos exponentes construyen el factor global \(K\) y las razones \(\beta_p\), siempre módulo \(10^9+7\).

Después precalculan \(S_\tau(x)\) para todos los \(x<10^6\) usando un tamiz de divisores seguido de sumas prefijo. Para argumentos mayores aplican la identidad hiperbólica y guardan los resultados en una caché, de modo que una consulta repetida no vuelve a ejecutar el bucle hasta \(\sqrt{x}\).

El recorrido principal avanza por los primos en orden creciente y mantiene el producto libre de cuadrados \(P_Q\), el signo alternante y el peso \(K\prod_{p\in Q}\beta_p\). En cada subconjunto visitado agrega o resta el término correspondiente, y se detiene en cuanto el siguiente producto excedería \(N\).

Análisis de Complejidad

Sea \(B=10^6\). Construir la tabla prefijo pequeña cuesta \(O(B\log B)\) tiempo y \(O(B)\) memoria, porque cada divisor actualiza todos sus múltiplos. La preparación de primos y exponentes hasta \(200\) es muy pequeña en comparación.

Sea \(R\) el número de productos libres de cuadrados de primos \(\le 200\) que no superan \(N\). El recorrido de subconjuntos usa \(O(R)\) pasos aritméticos. Si \(X\) es el conjunto de argumentos grandes distintos enviados a \(S_\tau\), el coste no cacheado para esos valores es

$$O\left(\sum_{x\in X}\sqrt{x}\right),$$

porque cada uno se calcula una sola vez con la identidad hiperbólica y luego queda memoizado. En la práctica, la poda \(P_Q\le N\) y la caché son las dos razones por las que el método funciona.

Notas y Referencias

  1. Página del problema: https://projecteuler.net/problem=608
  2. Función divisor: Wikipedia - Función divisor
  3. Divisor summatory function: Wikipedia - Divisor summatory function
  4. Fórmula de Legendre: Wikipedia - Fórmula de Legendre
  5. Principio de inclusión-exclusión: Wikipedia - Principio de inclusión-exclusión
  6. Método de la hipérbola de Dirichlet: Wikipedia - Dirichlet hyperbola method

问题概述

题目要求计算

$$D(m,n)=\sum_{d \mid m}\sum_{k=1}^{n}\sigma_0(kd),$$

其中 \(m=200!\),\(n=10^{12}\),结果对 \(10^9+7\) 取模。这里 \(\sigma_0=\tau\) 表示约数个数函数。若直接枚举 \(200!\) 的所有约数,再对每个 \(k\le n\) 逐项计算,规模完全不可行,所以解法必须先把双重求和改写成更有结构的形式。

实际做法是把 \(200!\) 的所有素因子贡献拆开,先得到一个按素数子集进行的容斥公式,再把内部求和压缩成“约数个数前缀和”函数的查询。这样,问题的核心就从暴力枚举转成了两个部分:一是如何构造正确的容斥权重,二是如何快速计算前缀和。

数学方法

设 \(m=f!\),这里 \(f=200\),并把上界记为 \(N=n\)。记 \(\mathbb{P}\) 为全体素数的集合,并定义

$$\mathcal{P}=\{p\in\mathbb{P}: p\le f\}.$$

对于每个 \(p\in\mathcal{P}\),它在 \(f!\) 中的指数是

$$e_p=v_p(f!)=\sum_{j\ge 1}\left\lfloor\frac{f}{p^j}\right\rfloor.$$

这正是 Legendre 公式。后面的所有推导都建立在这个素数幂分解上。

步骤 1:按素数把 \(\sum_{d\mid f!}\tau(kd)\) 拆开

\(f!\) 的任意一个约数都可以写成

$$d=\prod_{p\in\mathcal{P}}p^{a_p},\qquad 0\le a_p\le e_p.$$

固定一个整数 \(k\),对每个 \(p\in\mathcal{P}\) 记 \(b_p=v_p(k)\)。由于 \(\tau\) 在素数幂上是乘法型展开的,所以对所有 \(d\mid f!\) 求和时,可以把来自不同素数的影响分离出来:

$$\sum_{d \mid f!}\tau(kd)=\left(\prod_{q\notin\mathcal{P}}(v_q(k)+1)\right)\prod_{p\in\mathcal{P}}\sum_{a=0}^{e_p}(a+b_p+1).$$

这说明真正需要处理的只是单个素数上的局部和 \(\sum_{a=0}^{e}(a+b+1)\)。一旦这个局部表达式被整理好,整个公式就会自然地变成乘积结构。

步骤 2:用三角数重写局部表达式

定义三角数函数

$$T(t)=\frac{t(t+1)}{2}.$$

那么有

$$\sum_{a=0}^{e}(a+b+1)=(e+1)(b+1)+\frac{e(e+1)}{2}=T(e+1)(b+1)-T(e)b.$$

这是整个推导里最关键的代数整理。前一部分 \(T(e+1)(b+1)\) 可以理解为“保持 \(k\) 中当前的 \(p\) 次幂不变”,后一部分 \(T(e)b\) 则对应“如果 \(p\) 已经整除 \(k\),就从 \(k\) 里去掉一个 \(p\)”。正是这个“保留”与“去掉一份”的二项结构,使容斥展开成为可能。

步骤 3:把所有素数上的二项结构做容斥展开

定义

$$K=\prod_{p\in\mathcal{P}}T(e_p+1),\qquad \beta_p=\frac{T(e_p)}{T(e_p+1)}.$$

当我们在某个素数 \(p\) 上选取修正项时,会额外得到一个因子 \(-\beta_p\),而且这一项只有在 \(p\mid k\) 时才有意义,因为它对应于从 \(k\) 中拿掉一个 \(p\)。如果把所有选取修正项的素数组成集合 \(Q\subseteq\mathcal{P}\),并记

$$P_Q=\prod_{p\in Q}p,$$

那么 \(Q\) 中每个素数都恰好从 \(k\) 里消去一次,于是得到

$$\sum_{d \mid f!}\tau(kd)=K\sum_{Q\subseteq\mathcal{P},\ P_Q\mid k}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\tau\left(\frac{k}{P_Q}\right).$$

这一步就是代码所实现的核心恒等式。程序递归枚举素数子集,本质上就是在对这个容斥式逐项求值。

步骤 4:交换求和顺序,得到只含前缀和的公式

现在对 \(1\le k\le N\) 再求和。对固定的子集 \(Q\),因为必须满足 \(P_Q\mid k\),所以可以把 \(k\) 写成 \(k=P_Qr\)。于是

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\sum_{r\le N/P_Q}\tau(r).$$

定义约数个数函数的前缀和

$$S_\tau(x)=\sum_{r=1}^{x}\tau(r).$$

那么总公式就变成

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right).$$

如果某个子集的乘积 \(P_Q\) 已经大于 \(N\),那么这一项显然为零,因此递归时可以立刻剪枝。这正是实现中性能优化最直接的一点。

步骤 5:用双曲线方法快速计算 \(S_\tau(x)\)

由“计数满足 \(ab\le x\) 的因子对”这一观点,可以得到

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

接着把格点按双曲线 \(ab=x\) 对称配对,就得到经典的 Dirichlet 双曲线恒等式:

$$S_\tau(x)=2\sum_{d=1}^{\lfloor\sqrt{x}\rfloor}\left\lfloor\frac{x}{d}\right\rfloor-\lfloor\sqrt{x}\rfloor^2.$$

这正是实现处理大参数时所使用的公式。对于较小的 \(x\),实现会先用筛法预处理出全部前缀值,随后直接查表;对于较大的 \(x\),则用上式计算并把结果缓存起来,避免重复求值。

例子:\(m=3!=6\),\(N=5\)

此时 \(\mathcal{P}=\{2,3\}\),并且 \(e_2=e_3=1\)。所以

$$T(2)=3,\qquad T(1)=1,\qquad K=3\cdot 3=9,\qquad \beta_2=\beta_3=\frac{1}{3}.$$

总公式退化成

$$D(6,5)=9S_\tau(5)-3S_\tau(2)-3S_\tau(1)+S_\tau(0).$$

又因为

$$S_\tau(5)=10,\qquad S_\tau(2)=3,\qquad S_\tau(1)=1,\qquad S_\tau(0)=0,$$

所以

$$D(6,5)=9\cdot 10-3\cdot 3-3\cdot 1=78.$$

如果直接枚举 \(6\) 的约数 \(1,2,3,6\),也会得到同样的值:

$$\sum_{k=1}^{5}\tau(k)=10,\qquad \sum_{k=1}^{5}\tau(2k)=17,\qquad \sum_{k=1}^{5}\tau(3k)=19,\qquad \sum_{k=1}^{5}\tau(6k)=32,$$

因此

$$10+17+19+32=78.$$

这个小例子非常适合说明公式为什么成立,也能直观看出容斥写法并不是近似,而是与暴力求和完全等价的重组。

代码如何工作

C++、Python 和 Java 实现都先枚举不超过 \(200\) 的素数,再用 Legendre 公式计算每个素数在 \(200!\) 中的指数。基于这些指数,程序构造出整体乘子 \(K\) 以及每个素数对应的比例 \(\beta_p\),整个过程中始终在 \(10^9+7\) 模下运算。

接下来,程序先对所有 \(x<10^6\) 预处理 \(S_\tau(x)\)。做法是先用一个约数筛统计每个整数的约数个数,再做前缀和。对于更大的参数,则使用上面的双曲线公式计算,并把结果放入缓存,因此相同的查询不会重复进入平方根级别的循环。

最后,主递归按素数递增顺序遍历所有可能的子集,维护当前的平方自由乘积 \(P_Q\)、当前符号以及权重 \(K\prod_{p\in Q}\beta_p\)。每访问到一个子集,就把对应项加到答案里或从答案里减掉;一旦继续乘上下一个素数会使乘积超过 \(N\),这一支就直接停止。

复杂度分析

设 \(B=10^6\)。构建小范围前缀表的时间复杂度是 \(O(B\log B)\),空间复杂度是 \(O(B)\),因为每个约数都会更新它的所有倍数。相比之下,求不超过 \(200\) 的素数以及对应指数的代价很小。

设 \(R\) 表示所有不超过 \(N\) 的、由素数 \(\le 200\) 组成的平方自由乘积数量。子集递归本身需要 \(O(R)\) 次基本运算。再设 \(X\) 为所有传给 \(S_\tau\) 的不同大参数集合,那么这些未命中缓存的查询总代价为

$$O\left(\sum_{x\in X}\sqrt{x}\right),$$

因为每个这样的值只会用双曲线恒等式计算一次,之后就直接复用缓存结果。实际运行中,真正让程序可行的是两点:一是 \(P_Q\le N\) 的剪枝,二是对大参数前缀值的记忆化。

注释与参考资料

  1. 题目页面: https://projecteuler.net/problem=608
  2. 约数函数: Wikipedia - 约数函数
  3. Divisor summatory function: Wikipedia - Divisor summatory function
  4. Legendre 公式: Wikipedia - Legendre's formula
  5. 容斥原理: Wikipedia - 容斥原理
  6. Dirichlet 双曲线方法: Wikipedia - Dirichlet hyperbola method

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

Требуется вычислить

$$D(m,n)=\sum_{d \mid m}\sum_{k=1}^{n}\sigma_0(kd),$$

где \(m=200!\), \(n=10^{12}\), а ответ нужен по модулю \(10^9+7\). Здесь \(\sigma_0=\tau\) обозначает функцию числа делителей. Перебор всех \(d\mid 200!\) и всех \(k\le n\) невозможен, поэтому решение переводит исходную двойную сумму в гораздо более компактную формулу включений-исключений по простым числам, входящим в \(200!\).

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

Положим \(m=f!\), где \(f=200\), а верхнюю границу обозначим \(N=n\). Пусть \(\mathbb{P}\) обозначает множество всех простых чисел, и положим

$$\mathcal{P}=\{p\in\mathbb{P}: p\le f\}.$$

Для каждого \(p\in\mathcal{P}\) показатель этого простого в \(f!\) равен

$$e_p=v_p(f!)=\sum_{j\ge 1}\left\lfloor\frac{f}{p^j}\right\rfloor.$$

Это стандартная формула Лежандра, и именно она задает всю локальную структуру задачи.

Шаг 1: Разделить сумму по простым степеням

Любой делитель числа \(f!\) имеет вид

$$d=\prod_{p\in\mathcal{P}}p^{a_p},\qquad 0\le a_p\le e_p.$$

Для фиксированного \(k\) обозначим через \(b_p=v_p(k)\) показатели при простых \(p\in\mathcal{P}\). Так как \(\tau\) раскладывается по простым степеням, сумма по всем делителям \(f!\) факторизуется:

$$\sum_{d \mid f!}\tau(kd)=\left(\prod_{q\notin\mathcal{P}}(v_q(k)+1)\right)\prod_{p\in\mathcal{P}}\sum_{a=0}^{e_p}(a+b_p+1).$$

Значит, достаточно понять локальную сумму \(\sum_{a=0}^{e}(a+b+1)\) для одного простого числа.

Шаг 2: Переписать локальную сумму через треугольные числа

Введем функцию треугольных чисел

$$T(t)=\frac{t(t+1)}{2}.$$

Тогда

$$\sum_{a=0}^{e}(a+b+1)=(e+1)(b+1)+\frac{e(e+1)}{2}=T(e+1)(b+1)-T(e)b.$$

Это главный алгебраический шаг. Слагаемое \(T(e+1)(b+1)\) сохраняет текущую степень \(p\) внутри \(k\), а поправка \(T(e)b\) соответствует снятию одной копии \(p\), если \(p\) уже делит \(k\).

Шаг 3: Раскрыть произведение по принципу включений-исключений

Определим

$$K=\prod_{p\in\mathcal{P}}T(e_p+1),\qquad \beta_p=\frac{T(e_p)}{T(e_p+1)}.$$

Если для простого \(p\) берется поправочный член, появляется множитель \(-\beta_p\), и это возможно только тогда, когда \(p\mid k\). Пусть \(Q\subseteq\mathcal{P}\) состоит из всех простых, на которых выбрана поправка, и

$$P_Q=\prod_{p\in Q}p.$$

Тогда из \(k\) убирается по одному множителю каждого простого из \(Q\), и для любого фиксированного \(k\) получается формула

$$\sum_{d \mid f!}\tau(kd)=K\sum_{Q\subseteq\mathcal{P},\ P_Q\mid k}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\tau\left(\frac{k}{P_Q}\right).$$

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

Шаг 4: Поменять порядок суммирования

Теперь просуммируем по всем \(1\le k\le N\). Для фиксированного подмножества \(Q\) условие \(P_Q\mid k\) позволяет написать \(k=P_Qr\). Поэтому

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\sum_{r\le N/P_Q}\tau(r).$$

Введем сумматор функции числа делителей:

$$S_\tau(x)=\sum_{r=1}^{x}\tau(r).$$

Тогда окончательная форма имеет вид

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right).$$

Если \(P_Q>N\), соответствующий вклад равен нулю. Именно поэтому рекурсия может немедленно обрывать такие ветви.

Шаг 5: Быстро вычислить \(S_\tau(x)\)

Подсчет пар множителей дает тождество

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

Если сгруппировать точки решетки по разные стороны гиперболы \(ab=x\), получаем стандартную формулу Дирихле:

$$S_\tau(x)=2\sum_{d=1}^{\lfloor\sqrt{x}\rfloor}\left\lfloor\frac{x}{d}\right\rfloor-\lfloor\sqrt{x}\rfloor^2.$$

Именно так реализации считают большие значения. Для малых аргументов используется заранее построенная префиксная таблица.

Разобранный пример: \(m=3!=6\) и \(N=5\)

Здесь \(\mathcal{P}=\{2,3\}\) и \(e_2=e_3=1\). Следовательно,

$$T(2)=3,\qquad T(1)=1,\qquad K=3\cdot 3=9,\qquad \beta_2=\beta_3=\frac{1}{3}.$$

Формула по подмножествам принимает вид

$$D(6,5)=9S_\tau(5)-3S_\tau(2)-3S_\tau(1)+S_\tau(0).$$

Так как

$$S_\tau(5)=10,\qquad S_\tau(2)=3,\qquad S_\tau(1)=1,\qquad S_\tau(0)=0,$$

получаем

$$D(6,5)=9\cdot 10-3\cdot 3-3\cdot 1=78.$$

Прямая проверка совпадает:

$$\sum_{k=1}^{5}\tau(k)=10,\qquad \sum_{k=1}^{5}\tau(2k)=17,\qquad \sum_{k=1}^{5}\tau(3k)=19,\qquad \sum_{k=1}^{5}\tau(6k)=32,$$

поэтому

$$10+17+19+32=78.$$

Этот небольшой пример наглядно показывает, что формула включений-исключений не приближает ответ, а в точности переупорядочивает исходную сумму.

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

Реализации на C++, Python и Java сначала строят список всех простых чисел не больше \(200\) и для каждого такого простого вычисляют показатель в \(200!\) по формуле Лежандра. Затем из этих показателей составляются общий множитель \(K\) и отношения \(\beta_p\), причем все операции выполняются по модулю \(10^9+7\).

После этого заранее вычисляются значения \(S_\tau(x)\) для всех \(x<10^6\): сначала решетом считается число делителей каждого значения, затем строятся префиксные суммы. Для больших аргументов применяется гиперболическая формула, а результаты складываются в кэш, чтобы одинаковые запросы не пересчитывались заново.

Основной рекурсивный обход идет по простым в возрастающем порядке и хранит текущий квадратсвободный продукт \(P_Q\), знак и вес \(K\prod_{p\in Q}\beta_p\). Для каждого достигнутого подмножества к ответу добавляется или из него вычитается соответствующий вклад. Если умножение на следующий простой уже превышает \(N\), ветвь сразу прекращается.

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

Пусть \(B=10^6\). Построение малой префиксной таблицы занимает \(O(B\log B)\) времени и \(O(B)\) памяти, потому что каждый делитель обновляет все свои кратные. Подготовка простых и показателей до \(200\) на этом фоне мала.

Обозначим через \(R\) число квадратсвободных произведений простых \(\le 200\), не превосходящих \(N\). Тогда перебор подмножеств требует \(O(R)\) арифметических шагов. Если \(X\) - множество различных больших аргументов, переданных в \(S_\tau\), то стоимость некэшированных вычислений равна

$$O\left(\sum_{x\in X}\sqrt{x}\right),$$

поскольку каждое такое значение считается по гиперболической формуле только один раз, а затем переиспользуется из памяти. На практике решающими являются отсечение по условию \(P_Q\le N\) и мемоизация.

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

  1. Страница задачи: https://projecteuler.net/problem=608
  2. Функция числа делителей: Wikipedia - Функция числа делителей
  3. Divisor summatory function: Wikipedia - Divisor summatory function
  4. Формула Лежандра: Wikipedia - Формула Лежандра
  5. Принцип включения-исключения: Wikipedia - Формула включений-исключений
  6. Метод гиперболы Дирихле: Wikipedia - Dirichlet hyperbola method

ملخص المسألة

نريد حساب

$$D(m,n)=\sum_{d \mid m}\sum_{k=1}^{n}\sigma_0(kd),$$

عندما يكون \(m=200!\) و\(n=10^{12}\)، مع أخذ الناتج بترديد \(10^9+7\). هنا \(\sigma_0=\tau\) هي دالة عدد القواسم. الحساب المباشر عبر جميع القواسم \(d\mid 200!\) وجميع القيم \(k\le n\) غير عملي تمامًا، لذلك تُعاد صياغة المجموع المزدوج إلى صيغة شمول واستبعاد على أوليات \(200!\)، مع طريقة سريعة لحساب مجموع دالة عدد القواسم حتى حد معين.

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

نكتب \(m=f!\) حيث \(f=200\)، ونرمز للحد الأعلى بـ \(N=n\). ولنرمز بمجموعة الأعداد الأولية إلى \(\mathbb{P}\)، ثم نعرّف

$$\mathcal{P}=\{p\in\mathbb{P}: p\le f\}.$$

ولكل \(p\in\mathcal{P}\) فإن أس \(p\) في \(f!\) هو

$$e_p=v_p(f!)=\sum_{j\ge 1}\left\lfloor\frac{f}{p^j}\right\rfloor.$$

هذه هي صيغة Legendre، ومنها تنطلق البنية الحسابية كلها.

الخطوة 1: تفكيك المجموع حسب الأوليات

كل قاسم من قواسم \(f!\) يمكن كتابته على الصورة

$$d=\prod_{p\in\mathcal{P}}p^{a_p},\qquad 0\le a_p\le e_p.$$

ولعدد ثابت \(k\)، نكتب \(b_p=v_p(k)\) لكل \(p\in\mathcal{P}\). وبما أن \(\tau\) تتفكك على قوى الأوليات، فإن الجمع على جميع القواسم ينفصل إلى عوامل محلية:

$$\sum_{d \mid f!}\tau(kd)=\left(\prod_{q\notin\mathcal{P}}(v_q(k)+1)\right)\prod_{p\in\mathcal{P}}\sum_{a=0}^{e_p}(a+b_p+1).$$

إذًا جوهر المسألة هو فهم التعبير المحلي \(\sum_{a=0}^{e}(a+b+1)\) عند أولي واحد فقط.

الخطوة 2: إعادة كتابة التعبير المحلي بأعداد مثلثية

نعرّف دالة الأعداد المثلثية:

$$T(t)=\frac{t(t+1)}{2}.$$

وعندئذ

$$\sum_{a=0}^{e}(a+b+1)=(e+1)(b+1)+\frac{e(e+1)}{2}=T(e+1)(b+1)-T(e)b.$$

هذه هي الخطوة الجبرية الأساسية. الحد \(T(e+1)(b+1)\) يبقي أس \(p\) الموجود داخل \(k\) كما هو، أما حد التصحيح \(T(e)b\) فيمثل نزع نسخة واحدة من \(p\) إذا كان \(p\) يقسم \(k\).

الخطوة 3: توسيع حاصل الضرب بشمول واستبعاد

نعرّف

$$K=\prod_{p\in\mathcal{P}}T(e_p+1),\qquad \beta_p=\frac{T(e_p)}{T(e_p+1)}.$$

اختيار حد التصحيح عند أولي \(p\) يضيف العامل \(-\beta_p\)، ولا يحدث ذلك إلا إذا كان \(p\mid k\). إذا كانت \(Q\subseteq\mathcal{P}\) هي مجموعة الأوليات التي اخترنا عندها هذا التصحيح، وكتبنا

$$P_Q=\prod_{p\in Q}p,$$

فهذا يعني أننا نحذف عاملًا واحدًا من كل أولي في \(Q\) من داخل \(k\). لذلك نحصل، لكل \(k\) ثابت، على

$$\sum_{d \mid f!}\tau(kd)=K\sum_{Q\subseteq\mathcal{P},\ P_Q\mid k}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\tau\left(\frac{k}{P_Q}\right).$$

وهذه هي بالضبط صيغة شمول-استبعاد التي تنفذها التطبيقات عبر استعراض تفرعي لمجموعات الأوليات.

الخطوة 4: تبديل ترتيب الجمع

بعد ذلك نجمع على جميع \(1\le k\le N\). عند تثبيت مجموعة \(Q\)، فإن الشرط \(P_Q\mid k\) يسمح بكتابة \(k=P_Qr\). ومن ثم

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)\sum_{r\le N/P_Q}\tau(r).$$

نعرّف الآن المجموع التراكمي لدالة عدد القواسم:

$$S_\tau(x)=\sum_{r=1}^{x}\tau(r).$$

فتصبح الصيغة النهائية

$$D(f!,N)=K\sum_{Q\subseteq\mathcal{P}}(-1)^{|Q|}\left(\prod_{p\in Q}\beta_p\right)S_\tau\left(\left\lfloor\frac{N}{P_Q}\right\rfloor\right).$$

إذا كان \(P_Q>N\) فإن الحد المقابل يساوي صفرًا، ولهذا تقطع الخوارزمية هذا الفرع فورًا.

الخطوة 5: حساب \(S_\tau(x)\) بسرعة

من عدّ أزواج العوامل نحصل على الهوية

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

وبتقسيم نقاط الشبكة حول القطع الزائد \(ab=x\) نحصل على صيغة Dirichlet المعروفة:

$$S_\tau(x)=2\sum_{d=1}^{\lfloor\sqrt{x}\rfloor}\left\lfloor\frac{x}{d}\right\rfloor-\lfloor\sqrt{x}\rfloor^2.$$

هذه هي الصيغة التي تُستخدم للقيم الكبيرة. أما القيم الصغيرة فتُجلب من جدول تراكمي محسوب مسبقًا.

مثال محلول: \(m=3!=6\) و\(N=5\)

هنا \(\mathcal{P}=\{2,3\}\) و\(e_2=e_3=1\)، ولذلك

$$T(2)=3,\qquad T(1)=1,\qquad K=3\cdot 3=9,\qquad \beta_2=\beta_3=\frac{1}{3}.$$

فتصبح صيغة المجموعات

$$D(6,5)=9S_\tau(5)-3S_\tau(2)-3S_\tau(1)+S_\tau(0).$$

ومع

$$S_\tau(5)=10,\qquad S_\tau(2)=3,\qquad S_\tau(1)=1,\qquad S_\tau(0)=0,$$

نحصل على

$$D(6,5)=9\cdot 10-3\cdot 3-3\cdot 1=78.$$

والتحقق المباشر يعطي النتيجة نفسها:

$$\sum_{k=1}^{5}\tau(k)=10,\qquad \sum_{k=1}^{5}\tau(2k)=17,\qquad \sum_{k=1}^{5}\tau(3k)=19,\qquad \sum_{k=1}^{5}\tau(6k)=32,$$

ومن ثم

$$10+17+19+32=78.$$

هذا المثال الصغير يوضح أن الصيغة الجديدة ليست تقريبًا، بل إعادة تنظيم دقيقة للمجموع الأصلي.

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

تبدأ تطبيقات C++ وPython وJava بحصر جميع الأوليات حتى \(200\)، ثم تحسب أس كل أولي في \(200!\) باستخدام صيغة Legendre. بعد ذلك تُبنى الكمية العامة \(K\) والنسب \(\beta_p\) لكل أولي، وكل الحسابات تتم بترديد \(10^9+7\).

ثم تُجهَّز قيم \(S_\tau(x)\) لكل \(x<10^6\) بواسطة غربال للقواسم يتبعه جمع تراكمي. أما للقيم الأكبر، فتُستخدم صيغة القطع الزائد السابقة، وتُخزَّن النتائج في ذاكرة مؤقتة حتى لا يُعاد حساب الحلقة حتى \(\sqrt{x}\) أكثر من مرة لنفس القيمة.

أما الجزء الرئيس فيسير على الأوليات بترتيب تصاعدي، ويحمل حاصل الضرب الخالي من المربعات \(P_Q\)، والإشارة، والوزن \(K\prod_{p\in Q}\beta_p\). وعند كل مجموعة مزارة يضيف أو يطرح الحد المناسب. وإذا كان ضرب الأولي التالي سيجعل الناتج أكبر من \(N\)، فإن ذلك الفرع يتوقف فورًا.

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

لنفرض \(B=10^6\). بناء الجدول التراكمي الصغير يحتاج إلى زمن \(O(B\log B)\) وذاكرة \(O(B)\)، لأن كل قاسم يحدث جميع مضاعفاته. أما تجهيز قائمة الأوليات وأسسها حتى \(200\) فتكلفته صغيرة مقارنة بذلك.

إذا رمزنا بـ \(R\) إلى عدد الحواصل الخالية من المربعات المكوّنة من أوليات \(\le 200\) والتي لا تتجاوز \(N\)، فإن استعراض المجموعات يحتاج إلى \(O(R)\) خطوة حسابية. وإذا كانت \(X\) هي مجموعة القيم الكبيرة المختلفة التي تُمرَّر إلى \(S_\tau\)، فإن تكلفة الحسابات غير الموجودة في الذاكرة المؤقتة هي

$$O\left(\sum_{x\in X}\sqrt{x}\right),$$

لأن كل قيمة من هذه القيم تُحسب مرة واحدة فقط ثم تُخزن. عمليًا، العاملان الحاسمان هما القطع المبكر عند الشرط \(P_Q\le N\) والتخزين المؤقت لنتائج الدالة التراكمية.

حواشٍ ومراجع

  1. صفحة المسألة: https://projecteuler.net/problem=608
  2. دالة القواسم: Wikipedia - دالة القواسم
  3. Divisor summatory function: Wikipedia - Divisor summatory function
  4. صيغة Legendre: Wikipedia - Legendre's formula
  5. مبدأ الشمول والاستبعاد: Wikipedia - مبدأ الشمول والاستبعاد
  6. طريقة ديريشليه للقطع الزائد: Wikipedia - Dirichlet hyperbola method