Problem Summary

Consider the infinite concatenation

$$C=12345678910111213\dots$$

For a given \(n\), let \(P=\text{str}(n)\). Define \(f(n)\) as the starting position of the \(n\)-th occurrence of \(P\) inside \(C\). The Project Euler sum is taken over \(n=3^k\).

Mathematical Approach

1) Why this is a string-search problem on one continuous stream

The pattern \(P\) can appear:

1. entirely inside one integer,

2. across the boundary between consecutive integers,

3. overlapping with itself.

So we must search in the continuous digit stream \(123456789101112\dots\), not number by number. Resetting the match state at integer boundaries would miss valid occurrences.

2) KMP automaton for \(P=\text{str}(n)\)

The code builds a Knuth-Morris-Pratt automaton for the decimal pattern \(P\). A state \(q\in\{0,\dots,L\}\), where \(L=|P|\), means: “the last processed digits currently match the first \(q\) digits of \(P\)”.

For every state \(q\) and digit \(d\in\{0,\dots,9\}\), the automaton stores:

$$q'=\delta(q,d),$$

and an output bit

$$o(q,d)\in\{0,1\},$$

which tells whether reading digit \(d\) completes one new occurrence of \(P\).

Because the KMP fallback is applied after a full match, overlapping occurrences are counted correctly.

3) Operator view of a digit block

For any finite digit string \(w\), define an operator \(T_w\). Starting from automaton state \(q\), this operator tells us two things after scanning \(w\):

$$T_w(q)=\bigl(q_{\text{end}}(q),c_w(q)\bigr),$$

where \(q_{\text{end}}(q)\) is the ending state and \(c_w(q)\) is the number of new matches found while reading \(w\).

This is exactly what the code stores as end_state and add_count.

4) Why operators compose so cleanly

If a block is split as \(w=uv\), then scanning \(u\) first and \(v\) second gives

$$T_{uv}(q)=\Bigl(q_v(q_u(q)),\,c_u(q)+c_v\bigl(q_u(q)\bigr)\Bigr).$$

So composition is associative. This is the central algebraic fact used by the solver: once we know the operator of a block, we never need to rescan its digits.

5) Full blocks and partial blocks

Let a decimal prefix already be fixed. Then:

1. full_block(prefix, rem_digits) means the concatenation of all numbers with that prefix followed by all \(10^{\text{rem\_digits}}\) possible suffixes;

2. partial_block(prefix, rem_digits, upper_suffix) means the same, but only up to a given suffix bound.

These operators are memoized. Since many queries reuse the same prefix-and-length structure, caching removes the exponential blowup.

6) Building the operator for \([1..N]\)

To process all integers from \(1\) to \(N\), the code does not concatenate them explicitly. Instead it composes operators in this order:

1. all complete digit-length blocks with fewer digits than \(N\);

2. within the top digit length, all complete leading-digit blocks smaller than the first digit of \(N\);

3. one final partial block for the prefix that leads exactly up to \(N\).

The result is an operator we may denote by

$$T_{[1..N]}.$$

Applying it to automaton state \(0\) yields the total number of occurrences of \(P\) in the finite prefix \(123\dots N\).

7) The monotone counting function \(A(N)\)

Define

$$A(N)=\text{number of occurrences of }P\text{ in }123\dots N.$$

Clearly \(A(N)\) is monotone nondecreasing in \(N\). Therefore we can binary-search the smallest \(N\) such that

$$A(N)\ge n.$$

That identifies the exact integer in which the \(n\)-th occurrence finishes.

8) Locating the occurrence inside the final number

After binary search finds this minimal \(N\), the code computes the automaton state after scanning \(123\dots(N-1)\). Then it scans the digits of \(N\) once more, counting only the matches produced inside that last number.

If the desired occurrence ends at digit index \(i\) inside \(\text{str}(N)\), and \(L=|P|\), then its global starting position is

$$\text{digits\_before}(N-1)+i-L+2.$$

Here

$$\text{digits\_before}(N)=\sum_{d=1}^{D-1}9\cdot 10^{d-1}\cdot d+\bigl(N-10^{D-1}+1\bigr)D,$$

where \(D\) is the number of digits of \(N\).

9) Checkpoints

The implementation verifies several known values:

$$f(1)=1,\qquad f(5)=81,\qquad f(12)=271,\qquad f(7780)=111111365.$$

It also brute-forces all cases \(1\le n\le 30\) against a direct stream simulation, which is a strong validation of the operator approach.

How the Code Works

The class PatternEngine does all heavy lifting. It builds the KMP automaton, interns operators so identical operators are reused, memoizes operator composition, and provides:

1. range_operator(N) for the whole prefix \(123\dots N\),

2. occ_and_state(N) for total match count and ending automaton state,

3. nth_occurrence_position(target) for the final binary search and local scan.

The outer solver simply evaluates this engine for patterns \(3,9,27,\dots,3^{13}\) and sums the returned positions.

Complexity Analysis

If \(L=|P|\), one operator composition costs \(O(L)\). Thanks to memoized full/partial blocks, evaluating \(A(N)\) is much closer to polylogarithmic in \(N\) than to linear in the total digit stream length. That is the whole reason the problem is feasible: the code never generates the massive prefix of \(C\) explicitly.

Further Reading

  1. Problem page: https://projecteuler.net/problem=305
  2. Knuth-Morris-Pratt algorithm: https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
  3. Champernowne constant: https://en.wikipedia.org/wiki/Champernowne_constant

Problemzusammenfassung

Betrachte die unendliche Konkatenation

$$C=12345678910111213\dots$$

Für ein gegebenes \(n\) sei \(P=\text{str}(n)\). Dann ist \(f(n)\) die Startposition des \(n\)-ten Auftretens von \(P\) in \(C\). In der Aufgabe wird über \(n=3^k\) summiert.

Mathematischer Ansatz

1) Warum dies eine Suche im kontinuierlichen Ziffernstrom ist

Das Muster \(P\) kann

1. vollständig in einer einzelnen Zahl liegen,

2. eine Grenze zwischen zwei aufeinanderfolgenden Zahlen überschreiten,

3. sich mit sich selbst überlappen.

Darum muss im gesamten Strom \(123456789101112\dots\) gesucht werden; ein Zurücksetzen an Zahlengrenzen wäre falsch.

2) KMP-Automat für \(P=\text{str}(n)\)

Der Code baut einen Knuth-Morris-Pratt-Automaten für das Dezimalmuster \(P\). Ein Zustand \(q\in\{0,\dots,L\}\) mit \(L=|P|\) bedeutet, dass die zuletzt gelesenen Ziffern gerade die ersten \(q\) Zeichen von \(P\) treffen.

Für jeden Zustand \(q\) und jede Ziffer \(d\in\{0,\dots,9\}\) speichert der Automat:

$$q'=\delta(q,d)$$

und ein Ausgabebit

$$o(q,d)\in\{0,1\},$$

das anzeigt, ob beim Lesen von \(d\) ein neues vollständiges Auftreten endet. Wegen des KMP-Fallbacks nach einem Treffer werden auch überlappende Vorkommen korrekt gezählt.

3) Operator-Sicht auf einen Ziffernblock

Für jeden endlichen Ziffernstring \(w\) definieren wir einen Operator \(T_w\). Startet man im Zustand \(q\), dann liefert

$$T_w(q)=\bigl(q_{\text{end}}(q),c_w(q)\bigr)$$

den Endzustand und die Anzahl neuer Treffer beim Einlesen von \(w\). Genau dies wird im Code als end_state und add_count gespeichert.

4) Warum Operatoren so gut komponieren

Zerfällt ein Block in \(w=uv\), dann gilt

$$T_{uv}(q)=\Bigl(q_v(q_u(q)),\,c_u(q)+c_v\bigl(q_u(q)\bigr)\Bigr).$$

Die Komposition ist also assoziativ. Das ist die zentrale algebraische Beobachtung: kennt man den Operator eines Blocks, muss man seine Ziffern nie wieder explizit scannen.

5) Volle und partielle Blöcke

Ist bereits ein Dezimalpräfix fest vorgegeben, dann bedeutet:

1. full_block(prefix, rem_digits): alle Zahlen mit diesem Präfix und allen möglichen Suffixen der verbleibenden Länge;

2. partial_block(prefix, rem_digits, upper_suffix): dieselbe Struktur, aber nur bis zu einer gegebenen Suffix-Grenze.

Diese Operatoren werden memoisiert. Da viele Anfragen dieselbe Präfix-Längen-Struktur wiederverwenden, vermeidet das die kombinatorische Explosion.

6) Aufbau des Operators für \([1..N]\)

Um alle Zahlen von \(1\) bis \(N\) zu verarbeiten, konkatenieren wir sie nicht explizit. Stattdessen setzt der Code die Operatoren in folgender Reihenfolge zusammen:

1. alle vollständigen Stellenzahlblöcke mit weniger Stellen als \(N\);

2. in der obersten Stellenzahl alle vollständigen Führungsziffern-Blöcke unterhalb der ersten Ziffer von \(N\);

3. ein letzter partieller Block, der exakt bis \(N\) reicht.

Das Resultat ist ein Operator

$$T_{[1..N]},$$

und angewandt auf Zustand \(0\) liefert er die Gesamtzahl der Vorkommen von \(P\) im endlichen Präfix \(123\dots N\).

7) Die monotone Zählfunktion \(A(N)\)

Definiere

$$A(N)=\text{Anzahl der Vorkommen von }P\text{ in }123\dots N.$$

Offensichtlich ist \(A(N)\) monoton nicht fallend. Daher kann man per Binärsuche das kleinste \(N\) finden mit

$$A(N)\ge n.$$

Dieses \(N\) ist genau die Zahl, in der das \(n\)-te Auftreten endet.

8) Exakte Lokalisierung innerhalb der letzten Zahl

Nachdem diese minimale Zahl \(N\) gefunden ist, bestimmt der Code zunächst den Automatenzustand nach dem Lesen von \(123\dots(N-1)\). Dann werden die Ziffern von \(N\) einmal lokal abgescannt, bis das gesuchte Auftreten erreicht ist.

Endet der Treffer an der Ziffer mit lokalem Index \(i\) in \(\text{str}(N)\) und ist \(L=|P|\), so lautet die globale Startposition

$$\text{digits\_before}(N-1)+i-L+2.$$

Dabei ist

$$\text{digits\_before}(N)=\sum_{d=1}^{D-1}9\cdot 10^{d-1}\cdot d+\bigl(N-10^{D-1}+1\bigr)D,$$

wobei \(D\) die Stellenzahl von \(N\) ist.

9) Kontrollwerte

Die Implementierung überprüft unter anderem:

$$f(1)=1,\qquad f(5)=81,\qquad f(12)=271,\qquad f(7780)=111111365.$$

Zusätzlich werden alle Fälle \(1\le n\le 30\) gegen eine direkte Brute-Force-Stromsimulation geprüft.

Wie der Code arbeitet

Die Klasse PatternEngine erledigt die gesamte schwere Arbeit. Sie baut den KMP-Automaten, interned identische Operatoren, memoisiert Kompositionen und stellt bereit:

1. range_operator(N) für das Präfix \(123\dots N\),

2. occ_and_state(N) für Trefferzahl und Endzustand,

3. nth_occurrence_position(target) für Binärsuche plus lokalen Endscan.

Der äußere Solver wendet dies nur noch auf die Muster \(3,9,27,\dots,3^{13}\) an und summiert die Positionen.

Komplexität

Für \(L=|P|\) kostet eine Operator-Komposition \(O(L)\). Dank memoisierten Voll-/Teilblöcken ist die Auswertung von \(A(N)\) viel näher an polylogarithmischem Verhalten in \(N\) als an linearer Stromsimulation. Genau das macht die Aufgabe lösbar: das riesige Präfix von \(C\) wird nie explizit erzeugt.

Weiterführende Hinweise

  1. Problem: https://projecteuler.net/problem=305
  2. KMP-Algorithmus: https://de.wikipedia.org/wiki/Knuth-Morris-Pratt-Algorithmus
  3. Champernowne-Konstante: https://de.wikipedia.org/wiki/Champernowne-Konstante

Problem Özeti

Sonsuz birleştirme dizisini düşünelim:

$$C=12345678910111213\dots$$

Verilen bir \(n\) için \(P=\text{str}(n)\) olsun. \(f(n)\), \(P\) deseninin \(C\) içindeki \(n\). geçişinin başlangıç konumudur. Euler toplamı \(n=3^k\) değerleri üzerinden alınır.

Matematiksel Yaklaşım

1) Neden bu tek bir sürekli akış üzerinde desen arama problemidir

Desen \(P\)

1. tek bir sayının tamamen içinde,

2. ardışık iki sayının sınırını aşarak,

3. kendi kendisiyle örtüşerek

görünebilir. Bu yüzden arama, sayıları tek tek ayırarak değil, bütün \(123456789101112\dots\) akışı üzerinde yapılmalıdır.

2) \(P=\text{str}(n)\) için KMP otomatı

Kod, ondalık desen \(P\) için bir KMP otomatı kurar. \(L=|P|\) olmak üzere \(q\in\{0,\dots,L\}\) durumu, şu ana kadar okunan son rakamların \(P\)'nin ilk \(q\) karakteriyle eşleştiği anlamına gelir.

Her durum \(q\) ve her rakam \(d\in\{0,\dots,9\}\) için şu bilgiler tutulur:

$$q'=\delta(q,d),$$

ve bir çıktı biti

$$o(q,d)\in\{0,1\},$$

bu bit, \(d\) okununca yeni bir tam eşleşme tamamlanıp tamamlanmadığını söyler. KMP geri düşüşü sayesinde örtüşen eşleşmeler de doğru sayılır.

3) Bir rakam bloğunu operatör olarak görmek

Her sonlu rakam dizisi \(w\) için bir \(T_w\) operatörü tanımlarız. Başlangıç durumu \(q\) iken

$$T_w(q)=\bigl(q_{\text{son}}(q),c_w(q)\bigr)$$

değeri, blok bittiğinde ulaşılan durumu ve bu blok okunurken üretilen yeni eşleşme sayısını verir. Koddaki end_state ve add_count tam olarak bunu temsil eder.

4) Operatörler neden bu kadar temiz birleşir

Eğer \(w=uv\) ise

$$T_{uv}(q)=\Bigl(q_v(q_u(q)),\,c_u(q)+c_v\bigl(q_u(q)\bigr)\Bigr).$$

Yani bileşim birleşmelidir. Bu gözlem kritiktir: bir bloğun operatörünü bildikten sonra onun rakamlarını tekrar tek tek okumaya gerek kalmaz.

5) Tam blok ve kısmi blok fikri

Ondalık bir önek sabitlendiğinde:

1. full_block(prefix, rem_digits), bu önekten sonra kalan tüm olası son eklerin oluşturduğu bloktur;

2. partial_block(prefix, rem_digits, upper_suffix), aynı yapının sadece belirli bir son eke kadar olan kısmıdır.

Bu operatörler memoize edilir. Aynı önek-uzunluk yapıları çok kez tekrarlandığı için cache olmadan çözüm patlar.

6) \([1..N]\) aralığının operatörü nasıl kuruluyor

\(1\)'den \(N\)'ye kadar tüm sayıları gerçekten yan yana yazmak yerine kod şu sırayla operatörleri birleştirir:

1. \(N\)'den daha kısa tüm basamak uzunluğu blokları,

2. en üst uzunlukta, \(N\)'in ilk rakamından küçük tüm tam önek blokları,

3. tam olarak \(N\)'ye kadar giden son bir kısmi blok.

Bunun sonucu

$$T_{[1..N]}$$

operatörüdür. Başlangıç durumu \(0\)'a uygulanınca, \(123\dots N\) sonlu önekindeki toplam eşleşme sayısını verir.

7) Monoton sayım fonksiyonu \(A(N)\)

Şunu tanımlayalım:

$$A(N)=123\dots N\text{ içinde }P\text{ deseninin geçiş sayısı.}$$

Bu fonksiyon açıkça monoton artandır. O hâlde ikili arama ile

$$A(N)\ge n$$

şartını sağlayan en küçük \(N\) bulunabilir. Bu \(N\), aradığımız \(n\). geçişin tamamlandığı sayıdır.

8) Son sayının içinde tam konum nasıl bulunur

Bu minimal \(N\) bulunduktan sonra kod, önce \(123\dots(N-1)\) okunduktan sonraki otomat durumunu çıkarır. Sonra sadece \(N\)'in rakamlarını bir kez daha tarar ve istenen geçişin bu son sayı içinde hangi noktada oluştuğunu bulur.

Eğer geçiş, \(\text{str}(N)\) içinde yerel indeks \(i\)'de bitiyor ve \(L=|P|\) ise, global başlangıç konumu

$$\text{digits\_before}(N-1)+i-L+2$$

olur. Burada

$$\text{digits\_before}(N)=\sum_{d=1}^{D-1}9\cdot 10^{d-1}\cdot d+\bigl(N-10^{D-1}+1\bigr)D$$

formülü, \(N\)'den önce gelen toplam rakam sayısını verir; \(D\), \(N\)'in basamak sayısıdır.

9) Checkpoint'ler

Uygulama şu bilinen değerleri doğrular:

$$f(1)=1,\qquad f(5)=81,\qquad f(12)=271,\qquad f(7780)=111111365.$$

Ayrıca \(1\le n\le 30\) için sonuçlar doğrudan brute-force akış simülasyonuyla karşılaştırılır.

Kodun Çalışma Mantığı

PatternEngine sınıfı tüm ağır işi yapar. KMP otomatını kurar, aynı operatörleri tekrar kullanmak için intern eder, bileşimleri cache'ler ve şu arayüzleri sunar:

1. range_operator(N) ile \(123\dots N\) önekinin operatörü,

2. occ_and_state(N) ile toplam geçiş sayısı ve bitiş durumu,

3. nth_occurrence_position(target) ile ikili arama + son lokal tarama.

Dış çözücü ise bunu yalnızca \(3,9,27,\dots,3^{13}\) desenleri için çağırıp konumları toplar.

Karmaşıklık Analizi

\(L=|P|\) için tek bir operatör bileşimi \(O(L)\) maliyetlidir. Memoized tam/kısmi bloklar sayesinde \(A(N)\) değerlendirmesi, \(N\)'de doğrusal olmaktan çok polilogaritmik davranışa yaklaşır. Problemi çözülebilir yapan şey tam olarak budur: \(C\)'nin devasa öneki hiçbir zaman açıkça üretilmez.

İleri Okuma

  1. Problem metni: https://projecteuler.net/problem=305
  2. KMP algoritması: https://tr.wikipedia.org/wiki/Knuth-Morris-Pratt_algoritması
  3. Champernowne sabiti: https://tr.wikipedia.org/wiki/Champernowne_sabiti

Resumen del Problema

Consideremos la concatenación infinita

$$C=12345678910111213\dots$$

Para un \(n\) dado, sea \(P=\text{str}(n)\). Entonces \(f(n)\) es la posición inicial de la \(n\)-ésima aparición de \(P\) dentro de \(C\). En el problema se suman los casos \(n=3^k\).

Enfoque Matemático

1) Por qué esto es búsqueda de patrones sobre un flujo continuo

El patrón \(P\) puede aparecer:

1. completamente dentro de un número,

2. cruzando la frontera entre dos números consecutivos,

3. solapándose consigo mismo.

Por eso hay que trabajar sobre el flujo continuo \(123456789101112\dots\), no reiniciar la búsqueda al pasar de un entero al siguiente.

2) Autómata KMP para \(P=\text{str}(n)\)

El código construye un autómata KMP para el patrón decimal \(P\). Un estado \(q\in\{0,\dots,L\}\), con \(L=|P|\), significa que los últimos dígitos leídos coinciden con los primeros \(q\) caracteres de \(P\).

Para cada estado \(q\) y cada dígito \(d\in\{0,\dots,9\}\), el autómata guarda

$$q'=\delta(q,d)$$

y un bit de salida

$$o(q,d)\in\{0,1\},$$

que indica si al leer \(d\) se completa una nueva aparición. Como después de un acierto se aplica el fallback de KMP, los solapamientos también se cuentan correctamente.

3) Ver un bloque de dígitos como operador

Para cualquier cadena finita de dígitos \(w\), definimos un operador \(T_w\). Si se empieza en el estado \(q\), entonces

$$T_w(q)=\bigl(q_{\text{end}}(q),c_w(q)\bigr)$$

da el estado final y el número de coincidencias nuevas halladas al recorrer \(w\). Esto es exactamente lo que el código almacena en end_state y add_count.

4) Por qué los operadores se componen tan bien

Si un bloque se descompone como \(w=uv\), entonces

$$T_{uv}(q)=\Bigl(q_v(q_u(q)),\,c_u(q)+c_v\bigl(q_u(q)\bigr)\Bigr).$$

La composición es asociativa. Esa es la observación algebraica central: una vez conocido el operador de un bloque, no hace falta volver a escanear sus dígitos.

5) Bloques completos y bloques parciales

Si ya hay fijado un prefijo decimal, entonces:

1. full_block(prefix, rem_digits) representa todos los números con ese prefijo y todos los sufijos posibles de la longitud restante;

2. partial_block(prefix, rem_digits, upper_suffix) representa la misma estructura pero solo hasta cierto sufijo superior.

Estos operadores se memoizan. Como muchas consultas repiten la misma estructura prefijo-longitud, el caché evita una explosión combinatoria.

6) Construcción del operador para \([1..N]\)

Para procesar todos los enteros de \(1\) a \(N\), el código no los concatena explícitamente. En vez de eso compone operadores en este orden:

1. todos los bloques completos de longitudes menores que la de \(N\);

2. dentro de la longitud superior, todos los bloques completos con primer dígito menor que el primer dígito de \(N\);

3. un último bloque parcial que llega exactamente hasta \(N\).

El resultado puede denotarse por

$$T_{[1..N]}.$$

Aplicado al estado \(0\), produce el número total de apariciones de \(P\) en el prefijo finito \(123\dots N\).

7) La función monótona \(A(N)\)

Definimos

$$A(N)=\text{número de apariciones de }P\text{ en }123\dots N.$$

Es claro que \(A(N)\) es monótona creciente. Por tanto, se puede hacer una búsqueda binaria del menor \(N\) tal que

$$A(N)\ge n.$$

Ese \(N\) es precisamente el entero donde termina la \(n\)-ésima aparición buscada.

8) Localización exacta dentro del último número

Una vez hallado ese \(N\) mínimo, el código calcula el estado del autómata tras leer \(123\dots(N-1)\). Luego vuelve a escanear los dígitos de \(N\) y cuenta únicamente las coincidencias producidas dentro de ese último número.

Si la coincidencia deseada termina en el índice local \(i\) dentro de \(\text{str}(N)\), y \(L=|P|\), la posición global inicial es

$$\text{digits\_before}(N-1)+i-L+2.$$

Aquí

$$\text{digits\_before}(N)=\sum_{d=1}^{D-1}9\cdot 10^{d-1}\cdot d+\bigl(N-10^{D-1}+1\bigr)D,$$

donde \(D\) es el número de cifras de \(N\).

9) Checkpoints

La implementación verifica varios valores conocidos:

$$f(1)=1,\qquad f(5)=81,\qquad f(12)=271,\qquad f(7780)=111111365.$$

Además, compara por fuerza bruta todos los casos \(1\le n\le 30\) con una simulación directa del flujo.

Cómo funciona el código

La clase PatternEngine concentra todo el trabajo pesado. Construye el autómata KMP, reutiliza operadores idénticos, memoiza composiciones y expone:

1. range_operator(N) para el prefijo \(123\dots N\),

2. occ_and_state(N) para el conteo total y el estado final,

3. nth_occurrence_position(target) para la búsqueda binaria y el escaneo final local.

El solver exterior solo aplica este motor a los patrones \(3,9,27,\dots,3^{13}\) y suma las posiciones devueltas.

Complejidad

Si \(L=|P|\), una composición de operadores cuesta \(O(L)\). Gracias a la memoización de bloques completos y parciales, evaluar \(A(N)\) está mucho más cerca de un coste polilogarítmico en \(N\) que de una simulación lineal del flujo. Esa es la razón por la que el problema resulta factible: el prefijo gigantesco de \(C\) nunca se construye explícitamente.

Lecturas

  1. Problema: https://projecteuler.net/problem=305
  2. Algoritmo KMP: https://es.wikipedia.org/wiki/Algoritmo_de_Knuth-Morris-Pratt
  3. Constante de Champernowne: https://es.wikipedia.org/wiki/Constante_de_Champernowne

问题概述

考虑无限串

$$C=12345678910111213\dots$$

对给定 \(n\),记 \(P=\text{str}(n)\)。定义 \(f(n)\) 为 \(P\) 在 \(C\) 中第 \(n\) 次出现的起始位置。题目最后要求对 \(n=3^k\) 求和。

数学方法

1) 为什么这是在连续数字流上的模式匹配

模式 \(P\) 可能:

1. 完全出现在某一个整数内部;

2. 跨越相邻两个整数的边界;

3. 与自身重叠出现。

所以必须把 \(123456789101112\dots\) 看成一个连续数字流,而不能在每个整数边界上把匹配状态清零。

2) 为 \(P=\text{str}(n)\) 建立 KMP 自动机

代码为十进制模式 \(P\) 建立 KMP 自动机。若 \(L=|P|\),状态 \(q\in\{0,\dots,L\}\) 表示:当前已读入流的结尾,和 \(P\) 的前 \(q\) 个字符匹配。

对于每个状态 \(q\) 和每个数字 \(d\in\{0,\dots,9\}\),自动机保存

$$q'=\delta(q,d),$$

以及一个输出位

$$o(q,d)\in\{0,1\},$$

表示读入 \(d\) 后是否新完成了一次匹配。由于完整匹配后会做 KMP fallback,因此重叠出现也会被正确计数。

3) 把一个数字块看成算子

对于任意有限数字串 \(w\),定义算子 \(T_w\)。若起始状态为 \(q\),则

$$T_w(q)=\bigl(q_{\text{end}}(q),c_w(q)\bigr)$$

表示扫描完 \(w\) 之后到达的状态,以及在扫描过程中新增的匹配次数。这正是代码里的 end_stateadd_count

4) 为什么算子组合如此自然

若一个块分成 \(w=uv\),那么

$$T_{uv}(q)=\Bigl(q_v(q_u(q)),\,c_u(q)+c_v\bigl(q_u(q)\bigr)\Bigr).$$

因此组合是结合的。这个代数事实是整个算法的核心:一旦知道了某个块的算子,就再也不需要重新逐位扫描它。

5) 完整块与部分块

在某个十进制前缀已经固定后:

1. full_block(prefix, rem_digits) 表示该前缀后接所有可能的剩余位后缀;

2. partial_block(prefix, rem_digits, upper_suffix) 表示只枚举到某个上界后缀为止。

这些算子都被记忆化。因为大量查询会重复相同的前缀-长度结构,所以缓存非常关键。

6) 如何构造 \([1..N]\) 的整体算子

为了处理从 \(1\) 到 \(N\) 的所有整数,代码并不会真的生成字符串,而是按下面顺序组合算子:

1. 所有位数小于 \(N\) 的完整位数块;

2. 在最高位数层里,所有首位小于 \(N\) 首位的完整前缀块;

3. 最后一个恰好到达 \(N\) 的部分块。

这样得到的就是

$$T_{[1..N]}.$$

把它作用到初始状态 \(0\),就得到有限前缀 \(123\dots N\) 中模式 \(P\) 的总出现次数。

7) 单调计数函数 \(A(N)\)

定义

$$A(N)=123\dots N\text{ 中 }P\text{ 的出现次数。}$$

显然 \(A(N)\) 单调不减,因此可以二分最小的 \(N\),使得

$$A(N)\ge n.$$

这个 \(N\) 正好是第 \(n\) 次出现落入的整数。

8) 如何在最后一个整数里定位精确位置

当找到这个最小 \(N\) 之后,代码先求出扫描 \(123\dots(N-1)\) 后自动机所处的状态。然后仅再扫描一次 \(\text{str}(N)\) 的数字,数出在这个最后整数中产生的匹配。

如果目标匹配在 \(\text{str}(N)\) 的局部下标 \(i\) 结束,且 \(L=|P|\),那么全局起始位置为

$$\text{digits\_before}(N-1)+i-L+2.$$

其中

$$\text{digits\_before}(N)=\sum_{d=1}^{D-1}9\cdot 10^{d-1}\cdot d+\bigl(N-10^{D-1}+1\bigr)D,$$

\(D\) 是 \(N\) 的位数。

9) 校验点

实现中验证了若干已知值:

$$f(1)=1,\qquad f(5)=81,\qquad f(12)=271,\qquad f(7780)=111111365.$$

另外,对所有 \(1\le n\le 30\),代码还用直接暴力流模拟做了交叉验证。

代码如何工作

PatternEngine 类完成所有核心工作:建立 KMP 自动机、复用完全相同的算子、缓存算子组合,并提供:

1. range_operator(N):前缀 \(123\dots N\) 的整体算子;

2. occ_and_state(N):总匹配数与结束状态;

3. nth_occurrence_position(target):二分查找加最后局部扫描。

外层求解器只需要把这个引擎应用到 \(3,9,27,\dots,3^{13}\) 这些模式上并求和。

复杂度分析

若 \(L=|P|\),一次算子组合的代价是 \(O(L)\)。借助完整块/部分块的记忆化,计算 \(A(N)\) 的成本远小于线性扫描整个数字流,更接近于关于 \(N\) 的多对数级行为。正因为从不显式构造 \(C\) 的巨大前缀,这道题才是可做的。

延伸阅读

  1. 题目页面: https://projecteuler.net/problem=305
  2. KMP 算法: https://zh.wikipedia.org/wiki/KMP算法
  3. Champernowne 常数: https://zh.wikipedia.org/wiki/Champernowne常数

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

Рассматривается бесконечная конкатенация

$$C=12345678910111213\dots$$

Для заданного \(n\) пусть \(P=\text{str}(n)\). Тогда \(f(n)\) — это позиция начала \(n\)-го вхождения \(P\) в \(C\). В задаче суммируются значения для \(n=3^k\).

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

1) Почему это поиск по одному непрерывному потоку цифр

Шаблон \(P\) может:

1. полностью лежать внутри одного числа,

2. пересекать границу между соседними числами,

3. перекрываться с самим собой.

Поэтому искать нужно в едином потоке \(123456789101112\dots\), а не по отдельным числам с обнулением состояния на границе.

2) KMP-автомат для \(P=\text{str}(n)\)

Код строит KMP-автомат для десятичного шаблона \(P\). Состояние \(q\in\{0,\dots,L\}\), где \(L=|P|\), означает: последние прочитанные цифры совпадают с первыми \(q\) символами шаблона.

Для каждого состояния \(q\) и цифры \(d\in\{0,\dots,9\}\) хранятся

$$q'=\delta(q,d)$$

и бит выхода

$$o(q,d)\in\{0,1\},$$

который показывает, завершилось ли новое вхождение после чтения \(d\). Благодаря KMP-fallback после полного совпадения перекрывающиеся вхождения тоже учитываются правильно.

3) Операторный взгляд на цифровой блок

Для любой конечной строки цифр \(w\) вводится оператор \(T_w\). Если стартовать из состояния \(q\), то

$$T_w(q)=\bigl(q_{\text{end}}(q),c_w(q)\bigr)$$

даёт конечное состояние и число новых совпадений при чтении \(w\). В коде это ровно структуры end_state и add_count.

4) Почему операторы так удобно композиционируются

Если блок раскладывается как \(w=uv\), то

$$T_{uv}(q)=\Bigl(q_v(q_u(q)),\,c_u(q)+c_v\bigl(q_u(q)\bigr)\Bigr).$$

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

5) Полные и частичные блоки

Если некоторый десятичный префикс уже фиксирован, то:

1. full_block(prefix, rem_digits) означает все числа с этим префиксом и всеми возможными оставшимися суффиксами;

2. partial_block(prefix, rem_digits, upper_suffix) означает ту же структуру, но только до заданной верхней границы суффикса.

Эти операторы мемоизируются. Поскольку одинаковая структура префикс-длина возникает много раз, кэш здесь принципиален.

6) Построение оператора для \([1..N]\)

Чтобы обработать все числа от \(1\) до \(N\), код не склеивает их явно. Вместо этого он компонирует операторы в таком порядке:

1. все полные блоки с меньшим числом цифр, чем у \(N\);

2. внутри максимальной длины — все полные блоки с первой цифрой меньше первой цифры \(N\);

3. один последний частичный блок, который доходит ровно до \(N\).

Результат можно обозначить как

$$T_{[1..N]}.$$

Применение этого оператора к стартовому состоянию \(0\) даёт общее число вхождений \(P\) в конечном префиксе \(123\dots N\).

7) Монотонная функция подсчёта \(A(N)\)

Определим

$$A(N)=\text{число вхождений }P\text{ в }123\dots N.$$

Очевидно, \(A(N)\) монотонно не убывает. Значит, можно бинарным поиском найти минимальное \(N\), для которого

$$A(N)\ge n.$$

Именно это число \(N\) содержит завершение нужного \(n\)-го вхождения.

8) Как находится точная позиция внутри последнего числа

После нахождения минимального \(N\) код сначала вычисляет состояние автомата после чтения \(123\dots(N-1)\). Затем цифры числа \(N\) сканируются ещё один раз, и считаются только те совпадения, которые возникают внутри этого последнего числа.

Если нужное вхождение заканчивается на локальной позиции \(i\) внутри \(\text{str}(N)\), а \(L=|P|\), то глобальная начальная позиция равна

$$\text{digits\_before}(N-1)+i-L+2.$$

Здесь

$$\text{digits\_before}(N)=\sum_{d=1}^{D-1}9\cdot 10^{d-1}\cdot d+\bigl(N-10^{D-1}+1\bigr)D,$$

где \(D\) — число цифр в записи \(N\).

9) Контрольные значения

В реализации проверяются, в частности, такие значения:

$$f(1)=1,\qquad f(5)=81,\qquad f(12)=271,\qquad f(7780)=111111365.$$

Кроме того, все случаи \(1\le n\le 30\) сверяются с прямой brute-force симуляцией потока.

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

Класс PatternEngine делает всю основную работу. Он строит KMP-автомат, интернирует одинаковые операторы, мемоизирует композиции и предоставляет:

1. range_operator(N) для префикса \(123\dots N\),

2. occ_and_state(N) для общего числа вхождений и конечного состояния,

3. nth_occurrence_position(target) для бинарного поиска и финального локального прохода.

Внешний решатель просто применяет этот механизм к шаблонам \(3,9,27,\dots,3^{13}\) и суммирует найденные позиции.

Сложность

Если \(L=|P|\), то одна композиция операторов стоит \(O(L)\). Благодаря мемоизации полных и частичных блоков вычисление \(A(N)\) оказывается гораздо ближе к полилогарифмическому поведению по \(N\), чем к линейной симуляции всего потока. Именно поэтому задача решаема: огромный префикс \(C\) никогда не строится явно.

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

  1. Условие: https://projecteuler.net/problem=305
  2. Алгоритм Кнута-Морриса-Пратта: https://ru.wikipedia.org/wiki/Алгоритм_Кнута_—_Морриса_—_Пратта
  3. Константа Чамперноуна: https://ru.wikipedia.org/wiki/Константа_Чамперноуна

ملخص المسألة

ننظر إلى السلسلة اللانهائية

$$C=12345678910111213\dots$$

ولعدد معطى \(n\)، نضع \(P=\text{str}(n)\). عندئذٍ تكون \(f(n)\) هي موضع بداية الظهور رقم \(n\) للنمط \(P\) داخل \(C\). وفي المسألة يتم الجمع على القيم \(n=3^k\).

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

1) لماذا هذه مسألة بحث على تدفق رقمي واحد متصل

يمكن أن يظهر النمط \(P\):

1. بالكامل داخل عدد واحد،

2. عابرًا الحد بين عددين متتاليين،

3. ومتداخلًا مع نفسه.

لذلك يجب البحث في التدفق الكامل \(123456789101112\dots\)، وليس رقمًا رقمًا مع إعادة ضبط الحالة عند كل حد.

2) أوتومات KMP للنمط \(P=\text{str}(n)\)

يبني الكود أوتومات KMP للنمط العشري \(P\). إذا كان \(L=|P|\)، فالحالة \(q\in\{0,\dots,L\}\) تعني أن آخر الأرقام المقروءة تطابق أول \(q\) محارف من \(P\).

ولكل حالة \(q\) ولكل رقم \(d\in\{0,\dots,9\}\) يخزن الأوتومات

$$q'=\delta(q,d)$$

وبت خرج

$$o(q,d)\in\{0,1\},$$

يشير إلى ما إذا كانت قراءة \(d\) قد أنهت تطابقًا جديدًا كاملًا. وبسبب fallback الخاص بـ KMP بعد اكتمال المطابقة، تُحسب الظهورات المتداخلة أيضًا بشكل صحيح.

3) النظر إلى كتلة أرقام كمؤثر

لكل سلسلة أرقام منتهية \(w\) نعرّف مؤثرًا \(T_w\). إذا بدأنا من الحالة \(q\)، فإن

$$T_w(q)=\bigl(q_{\text{end}}(q),c_w(q)\bigr)$$

يعطي الحالة النهائية وعدد التطابقات الجديدة أثناء قراءة \(w\). وهذا هو بالضبط ما يمثله end_state وadd_count في الكود.

4) لماذا يتركب المؤثر بهذه السهولة

إذا كانت الكتلة \(w=uv\)، فإن

$$T_{uv}(q)=\Bigl(q_v(q_u(q)),\,c_u(q)+c_v\bigl(q_u(q)\bigr)\Bigr).$$

إذن التركيب تجميعي. وهذه هي الملاحظة الجبرية الأساسية: بمجرد أن نعرف مؤثر كتلة ما، لا نحتاج إلى إعادة قراءة أرقامها مرة أخرى.

5) الكتل الكاملة والكتل الجزئية

إذا كان لدينا بادئة عشرية مثبتة، فإن:

1. full_block(prefix, rem_digits) تمثل جميع الأعداد التي تبدأ بهذه البادئة ثم كل اللواحق الممكنة من الطول الباقي؛

2. partial_block(prefix, rem_digits, upper_suffix) تمثل البنية نفسها ولكن فقط حتى لاحقة علوية معينة.

وهذه المؤثرات تُخزَّن مع memoization لأن البنية نفسها تتكرر كثيرًا.

6) كيف يُبنى مؤثر المجال \([1..N]\)

لمعالجة جميع الأعداد من \(1\) إلى \(N\)، لا يقوم الكود بتوليد السلسلة فعليًا. بل يركّب المؤثرات بالترتيب التالي:

1. جميع كتل أطوال الأرقام الكاملة الأصغر من طول \(N\)؛

2. داخل الطول الأعلى، جميع الكتل الكاملة التي يكون رقمها الأول أصغر من أول رقم في \(N\)؛

3. ثم كتلة جزئية أخيرة تصل بدقة إلى \(N\).

والنتيجة هي المؤثر

$$T_{[1..N]}.$$

وعند تطبيقه على الحالة الابتدائية \(0\)، نحصل على عدد ظهورات \(P\) في البادئة المنتهية \(123\dots N\).

7) دالة العد الرتيبة \(A(N)\)

نعرف

$$A(N)=\text{عدد ظهورات }P\text{ داخل }123\dots N.$$

ومن الواضح أن \(A(N)\) دالة غير تناقصية. لذلك يمكننا إجراء بحث ثنائي عن أصغر \(N\) يحقق

$$A(N)\ge n.$$

وهذا \(N\) هو بالضبط العدد الذي ينتهي داخله الظهور رقم \(n\) المطلوب.

8) تحديد الموضع الدقيق داخل العدد الأخير

بعد العثور على هذا \(N\) الأدنى، يحسب الكود أولًا حالة الأوتومات بعد قراءة \(123\dots(N-1)\). ثم يعيد مسح أرقام \(N\) فقط، ويعدّ التطابقات التي تظهر داخل هذا العدد الأخير.

إذا انتهى التطابق المطلوب عند الفهرس المحلي \(i\) داخل \(\text{str}(N)\)، وكان \(L=|P|\)، فإن موضع البداية العالمي يساوي

$$\text{digits\_before}(N-1)+i-L+2.$$

حيث

$$\text{digits\_before}(N)=\sum_{d=1}^{D-1}9\cdot 10^{d-1}\cdot d+\bigl(N-10^{D-1}+1\bigr)D,$$

و\(D\) هو عدد خانات \(N\).

9) نقاط التحقق

تتحقق الشيفرة من عدة قيم معروفة:

$$f(1)=1,\qquad f(5)=81,\qquad f(12)=271,\qquad f(7780)=111111365.$$

كما أنها تقارن كل الحالات \(1\le n\le 30\) بمحاكاة brute-force مباشرة للتدفق.

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

تقوم الفئة PatternEngine بكل العمل الثقيل: تبني أوتومات KMP، وتعيد استخدام المؤثرات المتطابقة، وتخزن تراكيب المؤثرات، وتوفر:

1. range_operator(N) لبادئة \(123\dots N\)،

2. occ_and_state(N) لعدد التطابقات والحالة النهائية،

3. nth_occurrence_position(target) للبحث الثنائي مع المسح المحلي الأخير.

أما الحل الخارجي فيطبق هذا المحرك فقط على الأنماط \(3,9,27,\dots,3^{13}\) ثم يجمع المواضع الناتجة.

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

إذا كان \(L=|P|\)، فإن تركيب مؤثر واحد يكلف \(O(L)\). وبفضل memoization للكتل الكاملة والجزئية، يصبح تقييم \(A(N)\) أقرب بكثير إلى سلوك متعدد اللوغاريتم في \(N\) من كونه محاكاة خطية كاملة للتدفق. وهذا هو سبب إمكانية حل المسألة: البادئة الهائلة من \(C\) لا تُبنى صراحة أبدًا.

مراجع إضافية

  1. صفحة المسألة: https://projecteuler.net/problem=305
  2. خوارزمية KMP: https://ar.wikipedia.org/wiki/خوارزمية_كنوث-مورس-برات
  3. ثابت شامبرنو: https://en.wikipedia.org/wiki/Champernowne_constant