Problem Summary

Let \(S(n)\) denote the number of integers

$$0 \le x < 10^n$$

whose decimal digit sum is \(23\) and which are divisible by \(23\). Leading zeros are therefore allowed in the \(n\)-digit representation, which matches the code. The Project Euler target is \(S(11^{12}) \bmod 10^9\).

Mathematical Approach

1. Writing the two conditions in digit form

Write

$$x = \sum_{i=0}^{n-1} d_i 10^i,\qquad 0 \le d_i \le 9,$$

where \(d_0\) is the units digit. Then the problem imposes two simultaneous constraints:

$$\sum_{i=0}^{n-1} d_i = 23,$$

$$\sum_{i=0}^{n-1} d_i 10^i \equiv 0 \pmod{23}.$$

A direct dynamic program over all \(n\) positions would be impossible for \(n = 11^{12}\), so the key is to compress positions that behave identically modulo \(23\).

2. Why positions fall into 22 residue classes

Modulo \(23\), powers of \(10\) repeat with period \(22\), because

$$10^{22} \equiv 1 \pmod{23}.$$

Therefore, if two positions satisfy \(i \equiv j \pmod{22}\), then

$$10^i \equiv 10^j \pmod{23}.$$

So all digits sitting in the same residue class modulo \(22\) contribute to divisibility by the same weight. Define

$$w_c = 10^c \bmod 23,\qquad c=0,1,\ldots,21.$$

If \(n = 22q + r\) with \(0 \le r < 22\), then class \(c\) contains

$$u_c = \begin{cases} q+1, & c < r, \\ q, & c \ge r. \end{cases}$$

For example, if \(n=25\), then \(q=1\), \(r=3\), so classes \(0,1,2\) contain two positions each, while the other \(19\) classes contain one position each.

3. Reducing each class to its digit sum

Inside one class \(c\), suppose the digits in that class add up to \(k\). Then their total contribution modulo \(23\) is simply

$$k\,w_c \pmod{23},$$

because every position in that class carries the same weight \(w_c\). So we do not need the full digit pattern of the class; we only need to know how many assignments produce class sum \(k\).

For a class of size \(u\), the generating polynomial is

$$G_u(x) = (1+x+x^2+\cdots+x^9)^u.$$

The coefficient \([x^k]G_u(x)\) is exactly the number of ways to place \(u\) digits from \(0\) to \(9\) whose sum is \(k\).

Because the global digit sum is only \(23\), coefficients above degree \(23\) can never matter. So the code truncates every polynomial to degree \(23\), turning the huge exponent \(u\) into a manageable binary-exponentiation problem.

4. Why only two class polynomials are needed

Every class size is either \(q\) or \(q+1\). Therefore we only need the two truncated polynomials

$$G_q(x) = (1+x+\cdots+x^9)^q,$$

$$G_{q+1}(x) = (1+x+\cdots+x^9)^{q+1},$$

both computed modulo \(10^9\). The function poly_pow_digit_sum does this by repeated squaring, and the multiplication routine discards all terms above \(x^{23}\).

5. DP over classes: total digit sum and remainder mod 23

After compressing each residue class into “choose a class sum \(k\) with multiplicity \([x^k]G_{u_c}(x)\)”, the problem becomes a 22-step dynamic program. Let

$$DP[s][t]$$

be the number of ways, after processing some classes, to obtain total digit sum \(s\) and remainder \(t \pmod{23}\).

When processing class \(c\), choosing class sum \(k\) produces the transition

$$s' = s+k,\qquad t' = (t + w_c k) \bmod 23.$$

The multiplicity of this transition is \([x^k]G_{u_c}(x)\). After all \(22\) classes have been processed, the desired count is

$$DP[23][0].$$

This is the whole reason the huge value of \(n\) becomes harmless: the algorithm never iterates over all positions, only over the \(22\) residue classes.

6. Checks and interpretation

The implementation verifies itself with three checkpoints:

$$S(9)=263626,$$

$$S(42)=6377168878570056,$$

and a consistency check that the compressed class DP matches a direct position-by-position DP for

$$n=200.$$

The direct DP is only feasible for moderate \(n\), but it is an excellent validation tool: it confirms that grouping positions by residue modulo \(22\) loses no information.

How the Code Works

poly_mul_mod multiplies two digit-sum polynomials and truncates to degree \(23\). poly_pow_digit_sum raises \(1+x+\cdots+x^9\) to exponent \(q\) or \(q+1\) by binary exponentiation. count_via_classes_mod builds the \(22\) modular weights \(w_c\), runs the DP over \((\text{digit sum}, \text{remainder})\), and returns \(DP[23][0]\). The functions direct_count_exact and direct_count_mod are only for checkpoints on smaller \(n\).

Complexity Analysis

Polynomial multiplication is on vectors of length \(24\), so one multiplication costs \(O(23^2)\). Binary exponentiation therefore costs

$$O(23^2 \log n).$$

The class DP visits \(22\) classes, \(24\) digit-sum states, \(23\) remainders, and up to \(24\) class-sum choices, so its cost is

$$O(22 \cdot 23^3),$$

which is effectively constant for this fixed target sum. Memory usage is \(O(23^2)\).

Further Reading

  1. Problem page: https://projecteuler.net/problem=294
  2. Generating functions in combinatorics: https://en.wikipedia.org/wiki/Generating_function
  3. Modular arithmetic and multiplicative order: https://en.wikipedia.org/wiki/Multiplicative_order

Problemzusammenfassung

Sei \(S(n)\) die Anzahl der ganzen Zahlen

$$0 \le x < 10^n,$$

deren Dezimalziffernsumme \(23\) ist und die durch \(23\) teilbar sind. Führende Nullen sind in der \(n\)-stelligen Darstellung also erlaubt; genau das zählt der Code. Gesucht ist \(S(11^{12}) \bmod 10^9\).

Mathematischer Ansatz

1. Die zwei Bedingungen in Ziffernform

Schreibe

$$x = \sum_{i=0}^{n-1} d_i 10^i,\qquad 0 \le d_i \le 9,$$

wobei \(d_0\) die Einerziffer ist. Dann lauten die beiden Bedingungen:

$$\sum_{i=0}^{n-1} d_i = 23,$$

$$\sum_{i=0}^{n-1} d_i 10^i \equiv 0 \pmod{23}.$$

Ein direktes DP über alle \(n\) Positionen wäre für \(n = 11^{12}\) aussichtslos. Deshalb müssen Stellen zusammengefasst werden, die modulo \(23\) identisch wirken.

2. Warum die Stellen in 22 Restklassen zerfallen

Modulo \(23\) haben die Zehnerpotenzen Periode \(22\), denn

$$10^{22} \equiv 1 \pmod{23}.$$

Also gilt für \(i \equiv j \pmod{22}\) sofort

$$10^i \equiv 10^j \pmod{23}.$$

Alle Ziffern in derselben Restklasse modulo \(22\) tragen daher mit demselben Gewicht zur Teilbarkeit bei. Definiere

$$w_c = 10^c \bmod 23,\qquad c=0,1,\ldots,21.$$

Schreibt man \(n = 22q + r\) mit \(0 \le r < 22\), dann enthält Klasse \(c\)

$$u_c = \begin{cases} q+1, & c < r, \\ q, & c \ge r. \end{cases}$$

Beispiel: Für \(n=25\) ist \(q=1\), \(r=3\). Also haben die Klassen \(0,1,2\) jeweils zwei Positionen, die übrigen \(19\) Klassen je eine Position.

3. Reduktion jeder Klasse auf ihre Ziffernsumme

Angenommen, die Ziffern einer Klasse \(c\) summieren sich zu \(k\). Dann ist ihr Gesamtbeitrag modulo \(23\) einfach

$$k\,w_c \pmod{23},$$

weil alle Stellen dieser Klasse dasselbe Gewicht \(w_c\) besitzen. Das genaue Muster der Ziffern innerhalb der Klasse ist also unnötig; es genügt zu wissen, wie viele Belegungen die Klassensumme \(k\) erzeugen.

Für eine Klasse der Größe \(u\) ist das erzeugende Polynom

$$G_u(x) = (1+x+x^2+\cdots+x^9)^u.$$

Der Koeffizient \([x^k]G_u(x)\) zählt genau die Belegungen der \(u\) Stellen mit Ziffern \(0\) bis \(9\), deren Summe \(k\) ist.

Da die globale Ziffernsumme nur \(23\) beträgt, können Koeffizienten oberhalb des Grades \(23\) nie eine Rolle spielen. Deshalb schneidet der Code alle Polynome nach \(x^{23}\) ab und macht selbst riesige Exponenten durch binäre Exponentiation handhabbar.

4. Warum nur zwei Klassenpolynome benötigt werden

Jede Klassenlänge ist entweder \(q\) oder \(q+1\). Also genügen die zwei abgeschnittenen Polynome

$$G_q(x) = (1+x+\cdots+x^9)^q,$$

$$G_{q+1}(x) = (1+x+\cdots+x^9)^{q+1},$$

jeweils modulo \(10^9\). Die Funktion poly_pow_digit_sum berechnet sie per Exponentiation durch Quadrieren, und die Multiplikation verwirft alle Terme oberhalb von \(x^{23}\).

5. DP über Klassen: Gesamtziffernsumme und Rest modulo 23

Nachdem jede Restklasse auf „wähle eine Klassensumme \(k\) mit Vielfachheit \([x^k]G_{u_c}(x)\)“ reduziert wurde, bleibt ein 22-stufiges DP. Definiere

$$DP[s][t]$$

als Anzahl der Möglichkeiten, nach einigen Klassen die Gesamtziffernsumme \(s\) und den Rest \(t \pmod{23}\) zu erreichen.

Bei der Verarbeitung der Klasse \(c\) führt die Wahl der Klassensumme \(k\) zum Übergang

$$s' = s+k,\qquad t' = (t + w_c k) \bmod 23.$$

Die Vielfachheit dieses Übergangs ist \([x^k]G_{u_c}(x)\). Nach allen \(22\) Klassen ist die gesuchte Anzahl

$$DP[23][0].$$

Genau dadurch wird das gewaltige \(n\) unschädlich: Der Algorithmus iteriert nie über alle Stellen, sondern nur über die \(22\) Restklassen.

6. Prüfwerte und ihre Bedeutung

Die Implementierung kontrolliert sich mit drei Prüfungen:

$$S(9)=263626,$$

$$S(42)=6377168878570056,$$

sowie einem Konsistenztest, dass das Klassen-DP mit einem direkten Stellen-DP übereinstimmt für

$$n=200.$$

Das direkte DP ist nur für moderate \(n\) machbar, aber als Validierung sehr stark: Es bestätigt, dass die Gruppierung nach Restklassen modulo \(22\) keinerlei Information verliert.

Wie der Code arbeitet

poly_mul_mod multipliziert zwei Ziffernsummen-Polynome und schneidet bei Grad \(23\) ab. poly_pow_digit_sum potenziert \(1+x+\cdots+x^9\) auf den Exponenten \(q\) oder \(q+1\). count_via_classes_mod erzeugt die \(22\) Gewichte \(w_c\), führt das DP über \((\text{Ziffernsumme}, \text{Rest})\) aus und liefert \(DP[23][0]\). Die Funktionen direct_count_exact und direct_count_mod werden nur für Kontrolltests bei kleineren \(n\) verwendet.

Komplexitätsanalyse

Polynom-Multiplikation arbeitet auf Vektoren der Länge \(24\), also kostet eine Multiplikation \(O(23^2)\). Binäre Exponentiation benötigt damit

$$O(23^2 \log n).$$

Das Klassen-DP durchläuft \(22\) Klassen, \(24\) Ziffernsummen, \(23\) Reste und bis zu \(24\) Klassensummenwerte, also

$$O(22 \cdot 23^3),$$

praktisch konstant für die feste Zielsumme \(23\). Der Speicherbedarf ist \(O(23^2)\).

Weiterführende Hinweise

  1. Problemseite: https://projecteuler.net/problem=294
  2. Erzeugende Funktionen: https://de.wikipedia.org/wiki/Erzeugende_Funktion
  3. Modulare Ordnung: https://de.wikipedia.org/wiki/Ordnung_(Zahlentheorie)

Problem Özeti

\(S(n)\),

$$0 \le x < 10^n$$

koşulunu sağlayan, basamak toplamı \(23\) olan ve \(23\)'e tam bölünen tam sayıların sayısıdır. Yani \(n\) basamaklı yazımda baştaki sıfırlara izin verilir; kodun saydığı nesne tam olarak budur. Project Euler hedefi \(S(11^{12}) \bmod 10^9\)'dur.

Matematiksel Yaklaşım

1. İki koşulu basamaklar üzerinden yazmak

Şu şekilde yazalım:

$$x = \sum_{i=0}^{n-1} d_i 10^i,\qquad 0 \le d_i \le 9,$$

burada \(d_0\) birler basamağıdır. O zaman problem iki eşzamanlı koşul ister:

$$\sum_{i=0}^{n-1} d_i = 23,$$

$$\sum_{i=0}^{n-1} d_i 10^i \equiv 0 \pmod{23}.$$

\(n = 11^{12}\) kadar büyükken tüm pozisyonlar üzerinde doğrudan bir DP kurmak imkansızdır. Kritik fikir, mod \(23\) açısından aynı davranan pozisyonları tek sınıfta toplamaktır.

2. Pozisyonlar neden 22 sınıfa ayrılıyor?

\(23\) modunda \(10\)'un kuvvetleri \(22\) periyotludur; çünkü

$$10^{22} \equiv 1 \pmod{23}.$$

Bu yüzden \(i \equiv j \pmod{22}\) ise

$$10^i \equiv 10^j \pmod{23}$$

olur. Yani mod \(22\)'de aynı sınıfa düşen tüm pozisyonlar, bölünebilirlik hesabına aynı ağırlıkla katkı verir. Şunu tanımlayalım:

$$w_c = 10^c \bmod 23,\qquad c=0,1,\ldots,21.$$

\(n = 22q + r\), \(0 \le r < 22\) yazarsak sınıf \(c\)'deki pozisyon sayısı

$$u_c = \begin{cases} q+1, & c < r, \\ q, & c \ge r. \end{cases}$$

Örneğin \(n=25\) için \(q=1\), \(r=3\) olur; dolayısıyla \(0,1,2\) sınıflarında iki pozisyon, kalan \(19\) sınıfta birer pozisyon vardır.

3. Her sınıfı yalnızca sınıf toplamına indirgemek

Bir sınıf \(c\) içindeki rakamların toplamı \(k\) olsun. O sınıfın mod \(23\) katkısı doğrudan

$$k\,w_c \pmod{23}$$

olur; çünkü o sınıftaki bütün pozisyonlar aynı \(w_c\) ağırlığına sahiptir. Demek ki sınıf içindeki rakamların tam dizilimi gerekmez; yalnızca sınıf toplamı \(k\)'yi kaç farklı şekilde elde ettiğimizi bilmek yeterlidir.

Boyutu \(u\) olan bir sınıf için üreteç polinomu

$$G_u(x) = (1+x+x^2+\cdots+x^9)^u$$

şeklindedir. Buradaki \([x^k]G_u(x)\) katsayısı, \(u\) adet \(0\) ile \(9\) arasındaki rakamın toplamını \(k\) yapan atama sayısını verir.

Toplam basamak toplamı sadece \(23\) olduğu için \(23\)'ten büyük dereceli terimler hiçbir zaman kullanılamaz. Bu yüzden kod tüm polinomları \(x^{23}\)'te keser; böylece dev üsler bile ikili üs alma ile yönetilebilir hale gelir.

4. Neden yalnızca iki sınıf polinomuna ihtiyacımız var?

Her sınıfın boyu ya \(q\) ya da \(q+1\)'dir. Dolayısıyla yalnızca şu iki kesilmiş polinomu hesaplamak yeterlidir:

$$G_q(x) = (1+x+\cdots+x^9)^q,$$

$$G_{q+1}(x) = (1+x+\cdots+x^9)^{q+1}.$$

Her ikisi de \(10^9\) modunda tutulur. poly_pow_digit_sum bunu repeated squaring ile yapar; çarpım fonksiyonu \(x^{23}\)'ten sonraki tüm terimleri anında atar.

5. Sınıflar arasında DP: toplam basamak toplamı ve mod 23 kalanı

Her sınıfı “\([x^k]G_{u_c}(x)\) kadar çoklukla sınıf toplamı \(k\) seç” problemine indirdikten sonra geriye 22 adımlık bir DP kalır. Şunu tanımlayalım:

$$DP[s][t]$$

işlenmiş bazı sınıflardan sonra toplam basamak toplamı \(s\) ve mod \(23\) kalanı \(t\) olan atama sayısı olsun.

Sınıf \(c\) işlenirken sınıf toplamı \(k\) seçilirse geçiş

$$s' = s+k,\qquad t' = (t + w_c k) \bmod 23$$

şeklindedir. Bu geçişin çarpanı da \([x^k]G_{u_c}(x)\)'dir. Bütün \(22\) sınıf işlendiğinde aradığımız sayı

$$DP[23][0]$$

olur. Böylece \(n\)'in devasa olması sorun olmaktan çıkar; algoritma tüm pozisyonları değil, yalnızca \(22\) artık sınıfını işler.

6. Kontrol noktaları ve anlamları

Uygulama kendini üç kontrolle doğrular:

$$S(9)=263626,$$

$$S(42)=6377168878570056,$$

ve ayrıca sıkıştırılmış sınıf DP'sinin doğrudan pozisyon-pozisyon DP ile

$$n=200$$

için aynı sonucu verdiği kontrol edilir. Doğrudan DP ancak orta ölçekli \(n\)'lerde mümkün olsa da, bu karşılaştırma mod \(22\) sınıflamasının bilgi kaybetmediğini çok güçlü biçimde doğrular.

Kod Nasıl Çalışır?

poly_mul_mod iki basamak-toplam polinomunu çarpar ve sonucu derece \(23\)'te keser. poly_pow_digit_sum, \(1+x+\cdots+x^9\) polinomunu \(q\) veya \(q+1\) kuvvetine çıkarır. count_via_classes_mod \(22\) ağırlığı \(w_c\) üretir, \((\text{basamak toplamı}, \text{kalan})\) DP'sini çalıştırır ve \(DP[23][0]\)'ı döndürür. direct_count_exact ile direct_count_mod ise yalnızca küçük kontrol örnekleri içindir.

Karmaşıklık Analizi

Polinom çarpımı uzunluğu \(24\) olan diziler üzerinde yapıldığı için tek çarpım maliyeti \(O(23^2)\)'dir. Dolayısıyla ikili üs alma

$$O(23^2 \log n)$$

zaman alır. Sınıf DP'si \(22\) sınıf, \(24\) basamak toplamı, \(23\) kalan ve en fazla \(24\) sınıf-toplam seçeneği dolaşır; bu da

$$O(22 \cdot 23^3)$$

maliyet verir. Hedef toplam \(23\) sabit olduğundan bu pratikte sabit sayılır. Bellek kullanımı \(O(23^2)\)'dir.

İleri Okuma

  1. Problem sayfası: https://projecteuler.net/problem=294
  2. Üreteç fonksiyonlar: https://tr.wikipedia.org/wiki/Üreteç_fonksiyon
  3. Çarpımsal mertebe: https://en.wikipedia.org/wiki/Multiplicative_order

Resumen del Problema

Sea \(S(n)\) el número de enteros

$$0 \le x < 10^n$$

cuyo sumatorio de dígitos es \(23\) y que además son divisibles por \(23\). Por tanto, en la representación de longitud \(n\) se permiten ceros iniciales; eso coincide exactamente con el código. El objetivo de Project Euler es \(S(11^{12}) \bmod 10^9\).

Enfoque Matemático

1. Escribir las dos condiciones con dígitos

Escribimos

$$x = \sum_{i=0}^{n-1} d_i 10^i,\qquad 0 \le d_i \le 9,$$

donde \(d_0\) es la cifra de unidades. Entonces las restricciones son

$$\sum_{i=0}^{n-1} d_i = 23,$$

$$\sum_{i=0}^{n-1} d_i 10^i \equiv 0 \pmod{23}.$$

Un DP directo sobre las \(n\) posiciones sería imposible para \(n = 11^{12}\), así que hay que comprimir posiciones que se comportan igual módulo \(23\).

2. Por qué las posiciones se agrupan en 22 clases

Módulo \(23\), las potencias de \(10\) tienen periodo \(22\), ya que

$$10^{22} \equiv 1 \pmod{23}.$$

Por tanto, si \(i \equiv j \pmod{22}\), entonces

$$10^i \equiv 10^j \pmod{23}.$$

Todas las cifras situadas en la misma clase residual módulo \(22\) aportan al criterio de divisibilidad con el mismo peso. Definimos

$$w_c = 10^c \bmod 23,\qquad c=0,1,\ldots,21.$$

Si \(n = 22q + r\) con \(0 \le r < 22\), la clase \(c\) contiene

$$u_c = \begin{cases} q+1, & c < r, \\ q, & c \ge r. \end{cases}$$

Por ejemplo, si \(n=25\), entonces \(q=1\), \(r=3\): las clases \(0,1,2\) tienen dos posiciones y las otras \(19\) tienen una.

3. Reducir cada clase a su suma de dígitos

Dentro de una clase \(c\), supongamos que la suma de sus dígitos es \(k\). Entonces su contribución total módulo \(23\) es simplemente

$$k\,w_c \pmod{23},$$

porque todas las posiciones de esa clase tienen el mismo peso \(w_c\). Así, no hace falta conocer el patrón exacto de dígitos; basta con saber cuántas asignaciones producen suma de clase \(k\).

Para una clase de tamaño \(u\), el polinomio generador es

$$G_u(x) = (1+x+x^2+\cdots+x^9)^u.$$

El coeficiente \([x^k]G_u(x)\) cuenta exactamente cuántas formas hay de asignar \(u\) dígitos entre \(0\) y \(9\) cuya suma sea \(k\).

Como la suma total de dígitos es solo \(23\), los coeficientes por encima del grado \(23\) nunca importan. Por eso el código trunca todos los polinomios en \(x^{23}\), convirtiendo exponentes gigantes en un problema manejable de exponenciación binaria.

4. Por qué solo hacen falta dos polinomios de clase

Cada clase tiene tamaño \(q\) o \(q+1\). Por eso basta calcular los dos polinomios truncados

$$G_q(x) = (1+x+\cdots+x^9)^q,$$

$$G_{q+1}(x) = (1+x+\cdots+x^9)^{q+1},$$

ambos módulo \(10^9\). La función poly_pow_digit_sum los obtiene mediante exponenciación rápida, y la multiplicación descarta cualquier término de grado mayor que \(23\).

5. DP entre clases: suma total y residuo módulo 23

Una vez comprimida cada clase a “elige una suma de clase \(k\) con multiplicidad \([x^k]G_{u_c}(x)\)”, queda un DP de 22 pasos. Definimos

$$DP[s][t]$$

como el número de maneras de obtener, tras procesar algunas clases, suma total de dígitos \(s\) y residuo \(t \pmod{23}\).

Al procesar la clase \(c\), elegir suma de clase \(k\) produce la transición

$$s' = s+k,\qquad t' = (t + w_c k) \bmod 23.$$

La multiplicidad es \([x^k]G_{u_c}(x)\). Después de procesar las \(22\) clases, el valor buscado es

$$DP[23][0].$$

Esa es la razón por la que el valor inmenso de \(n\) deja de ser un problema: el algoritmo no recorre todas las posiciones, solo las \(22\) clases residuales.

6. Comprobaciones e interpretación

La implementación se valida con tres controles:

$$S(9)=263626,$$

$$S(42)=6377168878570056,$$

y además una verificación de consistencia de que el DP comprimido por clases coincide con un DP directo posición por posición para

$$n=200.$$

El DP directo solo es viable para \(n\) moderados, pero como herramienta de validación es excelente: confirma que agrupar posiciones por residuo módulo \(22\) no pierde ninguna información.

Cómo Funciona el Código

poly_mul_mod multiplica dos polinomios de suma de dígitos y trunca en grado \(23\). poly_pow_digit_sum eleva \(1+x+\cdots+x^9\) al exponente \(q\) o \(q+1\). count_via_classes_mod construye los \(22\) pesos \(w_c\), ejecuta el DP sobre \((\text{suma de dígitos}, \text{residuo})\) y devuelve \(DP[23][0]\). Las funciones direct_count_exact y direct_count_mod solo se usan para checkpoints con \(n\) pequeños.

Complejidad

La multiplicación polinómica trabaja con vectores de longitud \(24\), así que una multiplicación cuesta \(O(23^2)\). La exponenciación rápida cuesta entonces

$$O(23^2 \log n).$$

El DP por clases recorre \(22\) clases, \(24\) estados de suma, \(23\) residuos y hasta \(24\) opciones de suma de clase, es decir

$$O(22 \cdot 23^3),$$

lo que es prácticamente constante para esta suma objetivo fija. La memoria usada es \(O(23^2)\).

Lecturas

  1. Problema: https://projecteuler.net/problem=294
  2. Funciones generadoras: https://es.wikipedia.org/wiki/Función_generadora
  3. Orden multiplicativo: https://es.wikipedia.org/wiki/Orden_multiplicativo

问题概述

记 \(S(n)\) 为满足

$$0 \le x < 10^n$$

且“十进制数位和等于 \(23\)、同时能被 \(23\) 整除”的整数个数。也就是说,在长度为 \(n\) 的十进制表示中允许前导零,这与代码的计数方式完全一致。Project Euler 目标是求 \(S(11^{12}) \bmod 10^9\)。

数学方法

1. 把两个条件写成数位形式

$$x = \sum_{i=0}^{n-1} d_i 10^i,\qquad 0 \le d_i \le 9,$$

其中 \(d_0\) 是个位数字。那么题目要求同时满足

$$\sum_{i=0}^{n-1} d_i = 23,$$

$$\sum_{i=0}^{n-1} d_i 10^i \equiv 0 \pmod{23}.$$

如果对全部 \(n\) 个位置直接做 DP,那么 \(n = 11^{12}\) 时完全不可行。关键是把在模 \(23\) 意义下作用相同的位置压缩到一起。

2. 为什么位置会分成 22 个剩余类

在模 \(23\) 下,\(10\) 的幂以 \(22\) 为周期,因为

$$10^{22} \equiv 1 \pmod{23}.$$

因此只要 \(i \equiv j \pmod{22}\),就有

$$10^i \equiv 10^j \pmod{23}.$$

这说明所有下标同余于同一个 \(c\) 的位置,在整除条件中都有相同的权重。定义

$$w_c = 10^c \bmod 23,\qquad c=0,1,\ldots,21.$$

若 \(n = 22q + r\),其中 \(0 \le r < 22\),则第 \(c\) 类的位置个数为

$$u_c = \begin{cases} q+1, & c < r, \\ q, & c \ge r. \end{cases}$$

例如 \(n=25\) 时,\(q=1\)、\(r=3\),于是类 \(0,1,2\) 各有两个位置,其余 \(19\) 类各有一个位置。

3. 把每一类压缩成“该类数位和”

设某一类 \(c\) 中所有数字之和为 \(k\)。那么这整类对模 \(23\) 的贡献就是

$$k\,w_c \pmod{23},$$

因为该类中的每个位置都带着同样的权重 \(w_c\)。因此我们不需要知道这一类内部具体放了哪些数字,只需要知道“该类数位和为 \(k\) 的方案数”。

对于大小为 \(u\) 的一类,其生成多项式为

$$G_u(x) = (1+x+x^2+\cdots+x^9)^u.$$

其中系数 \([x^k]G_u(x)\) 正好等于:用 \(u\) 个 \(0\) 到 \(9\) 的数字,使总和为 \(k\) 的方案数。

因为全局数位和只有 \(23\),所以次数高于 \(23\) 的项永远用不到。代码因此把每个多项式都截断到 \(x^{23}\),这样即使指数极大,也能通过二进制快速幂来处理。

4. 为什么只需要两个类多项式

每一类的大小不是 \(q\) 就是 \(q+1\)。因此只需要计算下面两个截断多项式:

$$G_q(x) = (1+x+\cdots+x^9)^q,$$

$$G_{q+1}(x) = (1+x+\cdots+x^9)^{q+1}.$$

它们都在模 \(10^9\) 下计算。函数 poly_pow_digit_sum 通过 repeated squaring 完成快速幂,乘法函数则直接丢弃所有高于 \(x^{23}\) 的项。

5. 按类做 DP:总数位和 + 模 23 余数

把每个类都变成“选择一个类和 \(k\),权重为 \([x^k]G_{u_c}(x)\)”之后,问题就变成一个只有 22 步的 DP。定义

$$DP[s][t]$$

表示处理完若干类之后,得到总数位和 \(s\)、且当前模 \(23\) 余数为 \(t\) 的方案数。

处理第 \(c\) 类时,若选择类和 \(k\),则转移为

$$s' = s+k,\qquad t' = (t + w_c k) \bmod 23.$$

该转移的方案数乘子就是 \([x^k]G_{u_c}(x)\)。全部 \(22\) 类处理完以后,答案就是

$$DP[23][0].$$

这正是为什么巨大的 \(n\) 变得无害:算法不再遍历所有位置,而只遍历 22 个剩余类。

6. 校验点及其意义

程序用三个检查来验证自身:

$$S(9)=263626,$$

$$S(42)=6377168878570056,$$

以及检验压缩后的类 DP 与逐位直接 DP 在

$$n=200$$

时完全一致。直接 DP 只适合中等规模的 \(n\),但它是很强的验证工具,因为它证明了按模 \(22\) 分组并没有损失任何信息。

代码如何工作

poly_mul_mod 负责相乘两个“数位和多项式”,并在次数 \(23\) 处截断。poly_pow_digit_sum 把 \(1+x+\cdots+x^9\) 提到 \(q\) 或 \(q+1\) 次幂。count_via_classes_mod 先构造 22 个权重 \(w_c\),再对 \((\text{数位和}, \text{余数})\) 做 DP,最终返回 \(DP[23][0]\)。direct_count_exactdirect_count_mod 只用于较小 \(n\) 的检查。

复杂度分析

多项式乘法处理的是长度为 \(24\) 的向量,因此一次乘法代价为 \(O(23^2)\)。二进制快速幂的总代价为

$$O(23^2 \log n).$$

类 DP 要遍历 \(22\) 个类、\(24\) 个数位和状态、\(23\) 个余数、以及最多 \(24\) 个类和选择,所以复杂度为

$$O(22 \cdot 23^3),$$

对于固定目标和 \(23\) 来说几乎就是常数。空间复杂度为 \(O(23^2)\)。

延伸阅读

  1. 题目页面: https://projecteuler.net/problem=294
  2. 生成函数: https://zh.wikipedia.org/wiki/生成函数
  3. 乘法阶: https://zh.wikipedia.org/wiki/乘法阶

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

Пусть \(S(n)\) — количество целых чисел

$$0 \le x < 10^n,$$

у которых сумма десятичных цифр равна \(23\), а само число делится на \(23\). Значит, в записи длины \(n\) разрешены ведущие нули; именно это и считает код. Цель Project Euler — найти \(S(11^{12}) \bmod 10^9\).

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

1. Две условия в форме по цифрам

Запишем

$$x = \sum_{i=0}^{n-1} d_i 10^i,\qquad 0 \le d_i \le 9,$$

где \(d_0\) — цифра единиц. Тогда нужно одновременно иметь

$$\sum_{i=0}^{n-1} d_i = 23,$$

$$\sum_{i=0}^{n-1} d_i 10^i \equiv 0 \pmod{23}.$$

Прямой DP по всем \(n\) позициям невозможен при \(n = 11^{12}\), поэтому надо сжать позиции, которые одинаково ведут себя по модулю \(23\).

2. Почему позиции разбиваются на 22 класса

По модулю \(23\) степени числа \(10\) имеют период \(22\), потому что

$$10^{22} \equiv 1 \pmod{23}.$$

Следовательно, если \(i \equiv j \pmod{22}\), то

$$10^i \equiv 10^j \pmod{23}.$$

Все цифры на позициях из одного и того же класса по модулю \(22\) вносят вклад в критерий делимости с одним и тем же весом. Введём

$$w_c = 10^c \bmod 23,\qquad c=0,1,\ldots,21.$$

Если \(n = 22q + r\), где \(0 \le r < 22\), то класс \(c\) содержит

$$u_c = \begin{cases} q+1, & c < r, \\ q, & c \ge r. \end{cases}$$

Например, при \(n=25\) имеем \(q=1\), \(r=3\): классы \(0,1,2\) содержат по две позиции, остальные \(19\) классов — по одной.

3. Сведение каждого класса к сумме его цифр

Пусть сумма цифр внутри класса \(c\) равна \(k\). Тогда весь вклад этого класса по модулю \(23\) равен

$$k\,w_c \pmod{23},$$

потому что все позиции класса имеют одинаковый вес \(w_c\). Значит, точный рисунок цифр внутри класса не нужен; нужно лишь знать, сколько существует назначений с суммой класса \(k\).

Для класса размера \(u\) производящий полином равен

$$G_u(x) = (1+x+x^2+\cdots+x^9)^u.$$

Коэффициент \([x^k]G_u(x)\) как раз и есть число способов расставить \(u\) цифр от \(0\) до \(9\) так, чтобы их сумма была \(k\).

Поскольку общая сумма цифр всего \(23\), коэффициенты степеней выше \(23\) никогда не понадобятся. Поэтому код обрезает все полиномы после \(x^{23}\), и даже огромные показатели становятся управляемыми при бинарном возведении в степень.

4. Почему нужны только два полинома классов

Размер любого класса равен либо \(q\), либо \(q+1\). Значит, достаточно вычислить два обрезанных полинома

$$G_q(x) = (1+x+\cdots+x^9)^q,$$

$$G_{q+1}(x) = (1+x+\cdots+x^9)^{q+1},$$

оба по модулю \(10^9\). Функция poly_pow_digit_sum делает это быстрым возведением в степень, а умножение сразу отбрасывает все степени выше \(23\).

5. DP по классам: общая сумма цифр и остаток по модулю 23

После сжатия каждого класса до вида «выбери сумму класса \(k\) с кратностью \([x^k]G_{u_c}(x)\)» остаётся 22-шаговый DP. Определим

$$DP[s][t]$$

как число способов, после обработки некоторых классов, получить общую сумму цифр \(s\) и остаток \(t \pmod{23}\).

При обработке класса \(c\) выбор суммы класса \(k\) даёт переход

$$s' = s+k,\qquad t' = (t + w_c k) \bmod 23.$$

Кратность этого перехода равна \([x^k]G_{u_c}(x)\). После обработки всех \(22\) классов ответом будет

$$DP[23][0].$$

Именно поэтому огромное значение \(n\) перестаёт быть проблемой: алгоритм не проходит по всем позициям, а только по \(22\) остаточным классам.

6. Контрольные точки и их смысл

Реализация проверяет себя тремя тестами:

$$S(9)=263626,$$

$$S(42)=6377168878570056,$$

и сравнением, что сжатый DP по классам совпадает с прямым DP по позициям для

$$n=200.$$

Прямой DP возможен только для умеренных \(n\), но как проверка он очень силён: он подтверждает, что группировка по остаткам modulo \(22\) не теряет никакой информации.

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

poly_mul_mod перемножает два полинома по сумме цифр и обрезает результат по степени \(23\). poly_pow_digit_sum возводит \(1+x+\cdots+x^9\) в степень \(q\) или \(q+1\). count_via_classes_mod строит \(22\) веса \(w_c\), запускает DP по \((\text{сумма цифр}, \text{остаток})\) и возвращает \(DP[23][0]\). Функции direct_count_exact и direct_count_mod нужны только для проверок на меньших \(n\).

Сложность

Полиномиальное умножение работает с векторами длины \(24\), так что одна операция стоит \(O(23^2)\). Быстрое возведение в степень therefore costs

$$O(23^2 \log n).$$

DP по классам проходит \(22\) класса, \(24\) значения суммы, \(23\) остатка и до \(24\) вариантов суммы класса, то есть требует

$$O(22 \cdot 23^3),$$

что практически константа при фиксированной целевой сумме \(23\). Память равна \(O(23^2)\).

Дополнительные материалы

  1. Условие: https://projecteuler.net/problem=294
  2. Производящие функции: https://ru.wikipedia.org/wiki/Производящая_функция
  3. Мультипликативный порядок: https://ru.wikipedia.org/wiki/Порядок_элемента

ملخص المسألة

لتكن \(S(n)\) عدد الأعداد الصحيحة

$$0 \le x < 10^n$$

التي يكون مجموع أرقامها العشرية \(23\) وتكون قابلة للقسمة على \(23\). وهذا يعني أن الأصفار البادئة مسموحة في التمثيل ذي الطول \(n\)، وهو بالضبط ما يحسبه الكود. هدف Project Euler هو إيجاد \(S(11^{12}) \bmod 10^9\).

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

1. كتابة الشرطين بصيغة الأرقام

نكتب

$$x = \sum_{i=0}^{n-1} d_i 10^i,\qquad 0 \le d_i \le 9,$$

حيث \(d_0\) هو رقم الآحاد. عندئذٍ يفرض السؤال شرطين معًا:

$$\sum_{i=0}^{n-1} d_i = 23,$$

$$\sum_{i=0}^{n-1} d_i 10^i \equiv 0 \pmod{23}.$$

إن بناء DP مباشر على جميع المواضع مستحيل عندما يكون \(n = 11^{12}\). لذلك لا بد من ضغط المواضع التي تتصرف بالطريقة نفسها modulo \(23\).

2. لماذا تنقسم المواضع إلى 22 فئة باقية؟

بترديد \(23\)، تتكرر قوى \(10\) بدورة طولها \(22\)، لأن

$$10^{22} \equiv 1 \pmod{23}.$$

ومن ثم إذا كان \(i \equiv j \pmod{22}\) فإن

$$10^i \equiv 10^j \pmod{23}.$$

إذن كل الأرقام الواقعة في الفئة نفسها modulo \(22\) لها الوزن نفسه في شرط القسمة. نعرّف

$$w_c = 10^c \bmod 23,\qquad c=0,1,\ldots,21.$$

وإذا كتبنا \(n = 22q + r\) مع \(0 \le r < 22\)، فإن عدد المواضع في الفئة \(c\) هو

$$u_c = \begin{cases} q+1, & c < r, \\ q, & c \ge r. \end{cases}$$

فمثلًا عند \(n=25\) يكون \(q=1\) و\(r=3\)، فتحتوي الفئات \(0,1,2\) على موضعين لكل منها، بينما تحتوي الفئات التسع عشرة الأخرى على موضع واحد لكل منها.

3. اختزال كل فئة إلى مجموع أرقامها

لنفترض أن مجموع الأرقام داخل الفئة \(c\) يساوي \(k\). عندئذ يكون إسهام هذه الفئة كلها modulo \(23\) هو ببساطة

$$k\,w_c \pmod{23},$$

لأن جميع المواضع في تلك الفئة تحمل الوزن نفسه \(w_c\). لذلك لا نحتاج إلى معرفة الترتيب الداخلي الكامل للأرقام داخل الفئة؛ يكفي أن نعرف عدد التعيينات التي تعطي مجموعًا فئويًا مقداره \(k\).

إذا كان حجم الفئة \(u\)، فإن كثير الحدود المولد هو

$$G_u(x) = (1+x+x^2+\cdots+x^9)^u.$$

ومعامل \([x^k]G_u(x)\) يساوي تمامًا عدد الطرق لاختيار \(u\) أرقام من \(0\) إلى \(9\) بحيث يكون مجموعها \(k\).

وبما أن مجموع الأرقام الكلي لا يتجاوز \(23\)، فلن نحتاج أبدًا إلى معاملات الدرجات الأعلى من \(23\). لهذا يقتطع الكود كل كثير حدود عند \(x^{23}\)، فتغدو الأسس الضخمة قابلة للمعالجة بالأس السريع الثنائي.

4. لماذا نحتاج فقط إلى كثيري حدود للفئات؟

حجم كل فئة يساوي إما \(q\) أو \(q+1\). لذلك يكفي حساب كثيري الحدود المقتطعين

$$G_q(x) = (1+x+\cdots+x^9)^q,$$

$$G_{q+1}(x) = (1+x+\cdots+x^9)^{q+1},$$

وكلاهما modulo \(10^9\). تقوم الدالة poly_pow_digit_sum بحسابهما بالأس السريع، بينما تتخلص عملية الضرب مباشرة من كل الحدود الأعلى من \(x^{23}\).

5. DP بين الفئات: مجموع الأرقام الكلي وباقي القسمة modulo 23

بعد ضغط كل فئة إلى صيغة «اختر مجموعًا فئويًا \(k\) بمعامل \([x^k]G_{u_c}(x)\)» يتبقى لدينا DP من 22 خطوة فقط. نعرّف

$$DP[s][t]$$

على أنه عدد الطرق التي تعطي، بعد معالجة بعض الفئات، مجموع أرقام كليًا \(s\) وباقيًا \(t \pmod{23}\).

عند معالجة الفئة \(c\)، يؤدي اختيار مجموع فئوي \(k\) إلى الانتقال

$$s' = s+k,\qquad t' = (t + w_c k) \bmod 23.$$

ومضاعف هذا الانتقال هو \([x^k]G_{u_c}(x)\). وبعد معالجة الفئات الاثنتين والعشرين كلها تكون الإجابة المطلوبة هي

$$DP[23][0].$$

وهذا هو سبب زوال أثر ضخامة \(n\): فالخوارزمية لا تمر على جميع المواضع، بل على 22 فئة باقية فقط.

6. نقاط التحقق ومعناها

يتحقق التنفيذ من نفسه عبر ثلاث نقاط:

$$S(9)=263626,$$

$$S(42)=6377168878570056,$$

وكذلك مقارنة DP المضغوط حسب الفئات مع DP مباشر حسب المواضع عند

$$n=200.$$

الـ DP المباشر ممكن فقط عندما تكون \(n\) متوسطة الحجم، لكنه أداة تحقق ممتازة لأنه يثبت أن التجميع حسب البواقي modulo \(22\) لا يفقد أي معلومة.

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

تضرب الدالة poly_mul_mod كثيري حدود مجموع الأرقام وتقتطع الناتج عند الدرجة \(23\). وترفع poly_pow_digit_sum كثير الحدود \(1+x+\cdots+x^9\) إلى الأس \(q\) أو \(q+1\). ثم تبني count_via_classes_mod الأوزان \(w_c\) للفئات الاثنتين والعشرين، وتشغّل DP على الزوج \((\text{مجموع الأرقام}, \text{الباقي})\)، ثم تعيد \(DP[23][0]\). أما direct_count_exact وdirect_count_mod فمخصصتان فقط لنقاط التحقق عندما تكون \(n\) أصغر.

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

يُجرى ضرب كثيرات الحدود على متجهات طولها \(24\)، لذا تكلفة الضرب الواحد هي \(O(23^2)\). ومن ثم فإن الأس السريع يكلف

$$O(23^2 \log n).$$

أما DP الفئات فيمر عبر \(22\) فئة، و\(24\) حالة لمجموع الأرقام، و\(23\) باقيًا، وحتى \(24\) اختيارًا لمجموع الفئة، لذا تكلفته

$$O(22 \cdot 23^3),$$

وهي عمليًا ثابتة لأن مجموع الأرقام الهدف \(23\) ثابت. استهلاك الذاكرة هو \(O(23^2)\).

مراجع إضافية

  1. صفحة المسألة: https://projecteuler.net/problem=294
  2. الدوال المولدة: https://ar.wikipedia.org/wiki/دالة_مولدة
  3. الرتبة الضربية: https://ar.wikipedia.org/wiki/رتبة_ضربية