Problem Summary

Let \(d(n)\) be the number of greedy steps needed to reduce \(n\) to \(0\) by repeatedly subtracting the largest perfect cube not exceeding the current value. The quantity required by the problem is

$$T(N)=\sum_{n=1}^{N-1} d(n),$$

with the final target \(N=10^{17}\). A direct simulation for every \(n<N\) is hopeless, so the solution groups numbers by the first cube removed and turns the summation into a much smaller recursive dynamic program.

Mathematical Approach

For bookkeeping it is convenient to set \(d(0)=0\) and to define the partial-sum function

$$T(x)=\sum_{n=1}^{x-1} d(n)\qquad (x\ge 1).$$

The answer we want is therefore \(T(10^{17})\).

Step 1: Partition by the first removed cube

Take any \(x>1\), and let

$$m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\qquad b=m^3,\qquad r=x-b.$$

Then \(b\le x-1 < (m+1)^3\), so every integer in the interval

$$b,\ b+1,\ \dots,\ x-1$$

has the same first greedy move: subtract \(b=m^3\).

Any number in that block can be written as \(b+u\) with \(0\le u<r\), and its greedy decomposition begins with

$$b+u \longrightarrow u.$$

Therefore

$$d(b+u)=1+d(u).$$

Step 2: Convert that block identity into a summation recurrence

Summing the previous identity over \(u=0,1,\dots,r-1\) gives

$$T(x)-T(b)=\sum_{u=0}^{r-1}\bigl(1+d(u)\bigr)=r+T(r).$$

Now define the cube-boundary values

$$B_m=T(m^3).$$

Then the main recurrence becomes

$$\boxed{T(x)=B_m+r+T(r),\qquad m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\ r=x-m^3.}$$

This is the central formula used by the implementations.

Step 3: Derive the boundary recurrence

At an exact cube boundary, take \(x=(m+1)^3\). Then

$$r=(m+1)^3-m^3=3m^2+3m+1.$$

Substituting into the main recurrence yields

$$B_{m+1}=B_m+\bigl((m+1)^3-m^3\bigr)+T\bigl((m+1)^3-m^3\bigr).$$

So once the boundary values are computed in increasing order, each new boundary depends only on an already smaller argument. This turns the original problem into a forward sweep over consecutive cubes.

Step 4: Why the recursive argument shrinks quickly

The remainder satisfies

$$1\le r\le (m+1)^3-m^3=3m^2+3m+1.$$

Since \(m\approx x^{1/3}\), we get the rough bound

$$r\le 3x^{2/3}+3x^{1/3}+1.$$

That means one recursive step reduces an argument of size about \(x\) to a remainder of size about \(x^{2/3}\). Repeating the process shrinks the problem very fast, which is why memoization is enough to make the method practical even for \(10^{17}\).

Step 5: Turn the recurrence into a memoized dynamic program

Let

$$q=\left\lfloor\sqrt[3]{N-1}\right\rfloor.$$

The algorithm first computes all boundary values

$$B_1,\ B_2,\ \dots,\ B_q$$

from left to right. Whenever \(T(x)\) is needed for a non-boundary argument, the same recurrence

$$T(x)=B_m+r+T(r)$$

is applied and the result is stored. Because many boundary gaps and remainders recur, caching turns a potentially branching recursion tree into a directed acyclic graph of already solved states.

Worked Example

Take \(x=10\). Then

$$m=\left\lfloor\sqrt[3]{9}\right\rfloor=2,\qquad b=2^3=8,\qquad r=10-8=2.$$

The recurrence says

$$T(10)=T(8)+2+T(2).$$

Now \(T(8)=d(1)+\cdots+d(7)=1+2+3+4+5+6+7=28\), and \(T(2)=d(1)=1\). Hence

$$T(10)=28+2+1=31.$$

This matches the direct greedy counts: \(d(8)=1\) and \(d(9)=2\), so the last two numbers contribute \(3\) on top of \(T(8)\).

How the Code Works

The C++, Python, and Java implementations all follow the same structure. First they compute an exact floor cube root for a given integer by starting from a floating estimate and then correcting upward or downward until the surrounding cubes are in the right order. That avoids boundary mistakes near perfect cubes.

Next they build an array of cube-boundary totals \(B_m=T(m^3)\) up to \(m=\lfloor\sqrt[3]{N-1}\rfloor\). Each new entry uses the boundary recurrence, so the program only needs the gap between consecutive cubes and one recursive partial sum for that gap.

A hash table or dictionary stores every already computed value of \(T(x)\). When the same remainder appears again, the implementation returns the cached value immediately instead of recomputing the whole chain.

After the boundary sweep is complete, the program applies the same recurrence once more to the target \(N=10^{17}\). The C++ version also includes small brute-force checks for tiny inputs to confirm that the optimized recurrence matches direct simulation.

Complexity Analysis

Let \(q=\lfloor\sqrt[3]{N-1}\rfloor\), and let \(M\) be the number of distinct arguments whose values are actually memoized. The deterministic preprocessing is the forward sweep over the \(q\) cube boundaries, so that part is \(O(q)\).

For any uncached argument \(x\), one recurrence step replaces \(x\) by a remainder \(r\) with size \(O(x^{2/3})\), so an individual recursion chain has depth \(O(\log\log N)\). Since each distinct state is solved only once, the total running time is \(O(M)\) cube-root evaluations plus \(O(q)\) boundary updates, and the memory usage is \(O(M+q)\). In practice \(M\) stays close to the cube-root scale and is tiny compared with \(N\).

Footnotes and References

  1. Project Euler problem page: https://projecteuler.net/problem=884
  2. Cube number: Wikipedia — Cube number
  3. Greedy algorithm: Wikipedia — Greedy algorithm
  4. Memoization: Wikipedia — Memoization
  5. Dynamic programming: Wikipedia — Dynamic programming

Problemzusammenfassung

Sei \(d(n)\) die Anzahl der Greedy-Schritte, die benötigt werden, um \(n\) durch wiederholtes Subtrahieren der größten nicht überschrittenen Kubikzahl auf \(0\) zu bringen. Gesucht ist

$$T(N)=\sum_{n=1}^{N-1} d(n),$$

für \(N=10^{17}\). Eine direkte Simulation für alle \(n<N\) ist unmöglich, daher bündelt die Lösung Zahlen danach, welche Kubikzahl im ersten Schritt entfernt wird, und reduziert die Aufgabe auf eine viel kleinere rekursive Dynamik.

Mathematischer Ansatz

Zur bequemeren Notation setzen wir \(d(0)=0\) und definieren

$$T(x)=\sum_{n=1}^{x-1} d(n)\qquad (x\ge 1).$$

Gesucht ist also \(T(10^{17})\).

Step 1: Zerlegung nach dem ersten entfernten Würfel

Für \(x>1\) setzen wir

$$m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\qquad b=m^3,\qquad r=x-b.$$

Dann gilt \(b\le x-1 < (m+1)^3\). Folglich ist für jede Zahl im Intervall

$$b,\ b+1,\ \dots,\ x-1$$

der erste Greedy-Schritt identisch: Man zieht \(b=m^3\) ab.

Jede solche Zahl hat die Form \(b+u\) mit \(0\le u<r\), also beginnt ihre Zerlegung mit

$$b+u \longrightarrow u.$$

Daher folgt

$$d(b+u)=1+d(u).$$

Step 2: Summenrekurrenz für einen ganzen Block

Wenn man diese Identität über \(u=0,1,\dots,r-1\) aufsummiert, erhält man

$$T(x)-T(b)=\sum_{u=0}^{r-1}\bigl(1+d(u)\bigr)=r+T(r).$$

Nun definieren wir die Kubikgrenzwerte

$$B_m=T(m^3).$$

Damit lautet die Hauptrekurrenz

$$\boxed{T(x)=B_m+r+T(r),\qquad m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\ r=x-m^3.}$$

Genau diese Formel wird später im Programm verwendet.

Step 3: Rekurrenz auf exakten Kubikgrenzen

Für einen exakten Würfelrand wählen wir \(x=(m+1)^3\). Dann ist

$$r=(m+1)^3-m^3=3m^2+3m+1.$$

Einsetzen in die Hauptrekurrenz liefert

$$B_{m+1}=B_m+\bigl((m+1)^3-m^3\bigr)+T\bigl((m+1)^3-m^3\bigr).$$

Damit kann man die Werte \(B_1,B_2,\dots\) der Reihe nach aufbauen; jeder neue Rand hängt nur von einem deutlich kleineren Argument ab.

Step 4: Warum die Rekursion schnell kleiner wird

Für den Rest gilt

$$1\le r\le (m+1)^3-m^3=3m^2+3m+1.$$

Weil \(m\approx x^{1/3}\) ist, erhält man grob

$$r\le 3x^{2/3}+3x^{1/3}+1.$$

Ein Rekursionsschritt schrumpft die Größenordnung also von etwa \(x\) auf etwa \(x^{2/3}\). Nach wenigen Wiederholungen landet man bei sehr kleinen Resten. Darum genügt Memoisierung auch für \(10^{17}\).

Step 5: Memoisierte dynamische Programmierung

Sei

$$q=\left\lfloor\sqrt[3]{N-1}\right\rfloor.$$

Die Methode berechnet zuerst alle Randwerte

$$B_1,\ B_2,\ \dots,\ B_q$$

von links nach rechts. Wird ein Wert \(T(x)\) für ein Nicht-Rand-Argument benötigt, wird erneut

$$T(x)=B_m+r+T(r)$$

verwendet und das Ergebnis gespeichert. Weil viele Abstände zwischen Würfeln und viele Reste mehrfach auftreten, verwandelt das Zwischenspeichern den Rekursionsbaum effektiv in einen Graphen aus bereits gelösten Zuständen.

Worked Example

Nehmen wir \(x=10\). Dann

$$m=\left\lfloor\sqrt[3]{9}\right\rfloor=2,\qquad b=2^3=8,\qquad r=10-8=2.$$

Die Rekurrenz sagt

$$T(10)=T(8)+2+T(2).$$

Es ist \(T(8)=d(1)+\cdots+d(7)=1+2+3+4+5+6+7=28\) und \(T(2)=d(1)=1\). Also

$$T(10)=28+2+1=31.$$

Das stimmt mit der direkten Sicht überein: \(d(8)=1\) und \(d(9)=2\), also liefern die letzten beiden Zahlen zusammen \(3\) zusätzliche Schritte.

Wie der Code funktioniert

Die C++-, Python- und Java-Implementierungen haben dieselbe Grundstruktur. Zuerst berechnen sie eine exakte ganzzahlige Kubikwurzel, indem sie mit einer Gleitkomma-Schätzung starten und diese danach nach oben oder unten korrigieren, bis die benachbarten Kuben korrekt eingeordnet sind. Dadurch werden Rundungsfehler an perfekten Kuben vermieden.

Anschließend wird ein Array der Randwerte \(B_m=T(m^3)\) bis \(m=\lfloor\sqrt[3]{N-1}\rfloor\) aufgebaut. Jeder neue Eintrag benutzt die Randrekurrenz und benötigt daher nur die Lücke zwischen zwei aufeinanderfolgenden Kuben sowie eine rekursive Summenanfrage für genau diese Lücke.

Alle bereits berechneten Werte \(T(x)\) werden in einer Hash-Struktur gespeichert. Wenn derselbe Rest später noch einmal vorkommt, wird der gespeicherte Wert sofort zurückgegeben.

Nach dem vollständigen Randdurchlauf wird dieselbe Rekurrenz noch einmal auf \(N=10^{17}\) angewandt. Die C++-Implementierung enthält zusätzlich kleine Brute-Force-Prüfungen für Mini-Beispiele, um die schnelle Methode gegen direkte Simulation abzugleichen.

Komplexitätsanalyse

Sei \(q=\lfloor\sqrt[3]{N-1}\rfloor\), und sei \(M\) die Anzahl tatsächlich memoisierten Argumente. Der deterministische Vorlauf ist der lineare Durchgang über die \(q\) Kubikgrenzen, also \(O(q)\).

Für jedes noch nicht gespeicherte Argument \(x\) ersetzt ein Rekursionsschritt \(x\) durch einen Rest \(r\) von Größe \(O(x^{2/3})\). Eine einzelne Kette hat deshalb Tiefe \(O(\log\log N)\). Da jeder Zustand nur einmal gelöst wird, ergibt sich insgesamt \(O(M)\) Kubikwurzel-Auswertungen plus \(O(q)\) Randupdates; der Speicherbedarf ist \(O(M+q)\). In der Praxis liegt \(M\) nahe an der Kubikwurzel-Skala und ist winzig gegenüber \(N\).

Fußnoten und Referenzen

  1. Project Euler problem page: https://projecteuler.net/problem=884
  2. Kubikzahl: Wikipedia — Kubikzahl
  3. Greedy-Algorithmus: Wikipedia — Greedy-Algorithmus
  4. Memoisierung: Wikipedia — Memoisierung
  5. Dynamische Programmierung: Wikipedia — Dynamische Programmierung

Problem Özeti

\(d(n)\), her adımda mevcut değeri aşmayan en büyük küp çıkarılarak \(n\)'in \(0\)'a indirilmesi için gereken açgözlü adım sayısı olsun. Problemde istenen büyüklük

$$T(N)=\sum_{n=1}^{N-1} d(n),$$

ve hedef \(N=10^{17}\) değeridir. Tüm \(n<N\) için doğrudan simülasyon yapmak imkansız olduğundan, çözüm sayıları ilk çıkarılan küpe göre gruplar ve toplamı çok daha küçük bir özyinelemeli dinamik programa dönüştürür.

Matematiksel Yaklaşım

Notasyonu rahatlatmak için \(d(0)=0\) kabul edelim ve

$$T(x)=\sum_{n=1}^{x-1} d(n)\qquad (x\ge 1)$$

tanımını yapalım. Aradığımız cevap böylece \(T(10^{17})\) olur.

Step 1: İlk çıkarılan küpe göre parçalama

Her \(x>1\) için

$$m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\qquad b=m^3,\qquad r=x-b$$

alalım. O zaman \(b\le x-1 < (m+1)^3\) olduğundan

$$b,\ b+1,\ \dots,\ x-1$$

aralığındaki her sayı aynı ilk açgözlü hamleyi yapar: \(b=m^3\) çıkarılır.

Bu bloktaki her sayı \(0\le u<r\) için \(b+u\) biçimindedir ve ayrışımı

$$b+u \longrightarrow u$$

ile başlar. Dolayısıyla

$$d(b+u)=1+d(u)$$

elde edilir.

Step 2: Blok toplamından ana özyinelemeyi çıkarma

Bu eşitliği \(u=0,1,\dots,r-1\) üzerinde toplarsak

$$T(x)-T(b)=\sum_{u=0}^{r-1}\bigl(1+d(u)\bigr)=r+T(r)$$

olur. Şimdi küp sınırı değerlerini

$$B_m=T(m^3)$$

diye tanımlayalım. Böylece temel özyineleme

$$\boxed{T(x)=B_m+r+T(r),\qquad m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\ r=x-m^3}$$

şeklini alır. Uygulamanın dayandığı ana formül budur.

Step 3: Küp sınırları için özel geçiş

Eğer \(x=(m+1)^3\) seçilirse

$$r=(m+1)^3-m^3=3m^2+3m+1$$

olur. Bunu ana formüle koyunca

$$B_{m+1}=B_m+\bigl((m+1)^3-m^3\bigr)+T\bigl((m+1)^3-m^3\bigr)$$

elde edilir. Böylece ardışık küpler üzerinde soldan sağa ilerleyen bir dinamik program kurulur; her yeni sınır yalnızca daha küçük bir argümana ihtiyaç duyar.

Step 4: Kalan neden hızlı küçülür?

Kalan için

$$1\le r\le (m+1)^3-m^3=3m^2+3m+1$$

geçerlidir. \(m\approx x^{1/3}\) olduğundan kaba üst sınır

$$r\le 3x^{2/3}+3x^{1/3}+1$$

olur. Yani tek bir özyineleme adımı, yaklaşık \(x\) boyutundaki bir problemi yaklaşık \(x^{2/3}\) boyutuna indirir. Bu hızlı küçülme sayesinde memoization ile yöntem \(10^{17}\) için de rahatça çalışır.

Step 5: Memoization ile dinamik program

Şimdi

$$q=\left\lfloor\sqrt[3]{N-1}\right\rfloor$$

olsun. Algoritma önce

$$B_1,\ B_2,\ \dots,\ B_q$$

değerlerini soldan sağa hesaplar. Küp sınırı olmayan bir \(T(x)\) gerektiğinde yine

$$T(x)=B_m+r+T(r)$$

uygulanır ve bulunan sonuç saklanır. Aynı boşluklar ve aynı kalanlar tekrar tekrar ortaya çıktığı için önbellekleme özyineleme ağacını çözülen durumlardan oluşan yönlü çevrimsiz bir yapıya dönüştürür.

Worked Example

\(x=10\) alalım. O zaman

$$m=\left\lfloor\sqrt[3]{9}\right\rfloor=2,\qquad b=2^3=8,\qquad r=10-8=2$$

olur. Formül

$$T(10)=T(8)+2+T(2)$$

der. Burada \(T(8)=d(1)+\cdots+d(7)=1+2+3+4+5+6+7=28\) ve \(T(2)=d(1)=1\) olduğundan

$$T(10)=28+2+1=31$$

çıkar. Bu sonuç doğrudan sayımla da aynıdır: \(d(8)=1\) ve \(d(9)=2\), yani son iki sayı toplam \(3\) katkı yapar.

Kodun Çalışma Şekli

C++, Python ve Java uygulamaları aynı iskeleti izler. Önce kayan noktalı bir küpkök tahmini alınır; sonra çevredeki küpler karşılaştırılarak değer yukarı veya aşağı düzeltilir. Böylece tam küp sınırlarında yuvarlama hatası kalmaz.

Daha sonra \(m=\lfloor\sqrt[3]{N-1}\rfloor\) değerine kadar tüm küp sınırı toplamlarını içeren bir dizi oluşturulur. Her yeni sınır değeri, ardışık iki küp arasındaki fark ve bu fark için gereken özyinelemeli toplam kullanılarak hesaplanır.

Önceden hesaplanmış her \(T(x)\) değeri bir hash tablosu ya da sözlük içinde saklanır. Aynı kalan daha sonra yeniden istenirse sonuç doğrudan önbellekten alınır.

Sınır taraması tamamlandıktan sonra aynı özyineleme hedef \(N=10^{17}\) için bir kez daha uygulanır. C++ sürümü ayrıca küçük girdilerde brute-force karşılaştırmaları yaparak hızlı yöntemin doğruluğunu kontrol eder.

Karmaşıklık Analizi

\(q=\lfloor\sqrt[3]{N-1}\rfloor\) ve \(M\) de gerçekten önbelleğe alınan farklı argüman sayısı olsun. Deterministik ön işlem, \(q\) tane küp sınırı üzerinden tek yönlü geçiştir; dolayısıyla bu kısım \(O(q)\) zaman alır.

Önbellekte olmayan bir \(x\) için tek adımda kalan \(r\), \(O(x^{2/3})\) boyutuna düşer. Bu yüzden tek bir özyineleme zincirinin derinliği \(O(\log\log N)\) olur. Her farklı durum yalnızca bir kez çözüldüğünden toplam çalışma maliyeti \(O(M)\) adet küpkök değerlendirmesi artı \(O(q)\) sınır güncellemesidir; bellek kullanımı ise \(O(M+q)\)'dur. Pratikte \(M\), küpkök ölçeğinde kalır ve \(N\)'ye göre çok küçüktür.

Dipnotlar ve Kaynaklar

  1. Project Euler problem page: https://projecteuler.net/problem=884
  2. Küp sayı: Wikipedia — Küp sayı
  3. Açgözlü algoritma: Wikipedia — Açgözlü algoritma
  4. Memoization: Wikipedia — Memoization
  5. Dinamik programlama: Wikipedia — Dinamik programlama

Resumen del Problema

Sea \(d(n)\) el número de pasos codiciosos necesarios para reducir \(n\) hasta \(0\) restando siempre el mayor cubo perfecto que no supera al valor actual. La cantidad pedida es

$$T(N)=\sum_{n=1}^{N-1} d(n),$$

con objetivo final \(N=10^{17}\). Simular todos los \(n<N\) es inviable, así que la solución agrupa los enteros según el primer cubo que se elimina y convierte la suma en un programa dinámico recursivo mucho más pequeño.

Enfoque Matemático

Para simplificar la notación tomamos \(d(0)=0\) y definimos

$$T(x)=\sum_{n=1}^{x-1} d(n)\qquad (x\ge 1).$$

Por tanto, la respuesta buscada es \(T(10^{17})\).

Step 1: Partición por el primer cubo restado

Fijemos \(x>1\) y pongamos

$$m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\qquad b=m^3,\qquad r=x-b.$$

Entonces \(b\le x-1 < (m+1)^3\), de modo que todos los enteros del intervalo

$$b,\ b+1,\ \dots,\ x-1$$

comparten el mismo primer movimiento codicioso: restar \(b=m^3\).

Cada número del bloque se escribe como \(b+u\) con \(0\le u<r\), y su descomposición empieza con

$$b+u \longrightarrow u.$$

Así obtenemos

$$d(b+u)=1+d(u).$$

Step 2: Pasar del bloque a una recurrencia de sumas

Al sumar la identidad anterior sobre \(u=0,1,\dots,r-1\) se obtiene

$$T(x)-T(b)=\sum_{u=0}^{r-1}\bigl(1+d(u)\bigr)=r+T(r).$$

Introduzcamos ahora los valores en frontera cúbica

$$B_m=T(m^3).$$

La recurrencia principal queda

$$\boxed{T(x)=B_m+r+T(r),\qquad m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\ r=x-m^3.}$$

Esa es la identidad central que explotan las implementaciones.

Step 3: Recurrencia en las fronteras entre cubos

Si tomamos \(x=(m+1)^3\), entonces

$$r=(m+1)^3-m^3=3m^2+3m+1.$$

Sustituyendo en la fórmula principal se llega a

$$B_{m+1}=B_m+\bigl((m+1)^3-m^3\bigr)+T\bigl((m+1)^3-m^3\bigr).$$

Esto permite construir los valores \(B_1,B_2,\dots\) en orden creciente: cada nueva frontera depende solo de un argumento mucho menor.

Step 4: Por qué el residuo se hace pequeño muy rápido

El residuo cumple

$$1\le r\le (m+1)^3-m^3=3m^2+3m+1.$$

Como \(m\approx x^{1/3}\), resulta la cota aproximada

$$r\le 3x^{2/3}+3x^{1/3}+1.$$

Es decir, una sola llamada recursiva transforma un argumento de tamaño cercano a \(x\) en otro de tamaño cercano a \(x^{2/3}\). Tras pocas iteraciones el problema cae a valores muy pequeños, y por eso la memorización basta incluso para \(10^{17}\).

Step 5: Programación dinámica con memorización

Sea

$$q=\left\lfloor\sqrt[3]{N-1}\right\rfloor.$$

El algoritmo calcula primero todos los valores de frontera

$$B_1,\ B_2,\ \dots,\ B_q$$

de izquierda a derecha. Cuando se necesita un valor \(T(x)\) que no está en frontera, se vuelve a aplicar

$$T(x)=B_m+r+T(r)$$

y el resultado se guarda. Como muchas diferencias entre cubos consecutivos y muchos residuos reaparecen, la caché convierte el árbol recursivo en un grafo acíclico de estados ya resueltos.

Worked Example

Tomemos \(x=10\). Entonces

$$m=\left\lfloor\sqrt[3]{9}\right\rfloor=2,\qquad b=2^3=8,\qquad r=10-8=2.$$

La recurrencia da

$$T(10)=T(8)+2+T(2).$$

Ahora bien, \(T(8)=d(1)+\cdots+d(7)=1+2+3+4+5+6+7=28\), y \(T(2)=d(1)=1\). Por tanto,

$$T(10)=28+2+1=31.$$

Esto coincide con el conteo directo: \(d(8)=1\) y \(d(9)=2\), así que los dos últimos números añaden \(3\) al total acumulado hasta \(8\).

Cómo Funciona el Código

Las implementaciones en C++, Python y Java siguen exactamente la misma estrategia. Primero calculan una raíz cúbica entera exacta partiendo de una aproximación en coma flotante y corrigiéndola después hacia arriba o hacia abajo hasta que los cubos vecinos quedan bien situados. Eso evita errores en torno a los cubos perfectos.

Después construyen un arreglo de valores de frontera \(B_m=T(m^3)\) hasta \(m=\lfloor\sqrt[3]{N-1}\rfloor\). Cada nueva entrada se obtiene mediante la recurrencia de frontera, usando la diferencia entre dos cubos consecutivos y una única consulta recursiva para esa diferencia.

Todos los valores ya calculados de \(T(x)\) se guardan en una tabla hash o diccionario. Si un mismo residuo vuelve a aparecer, la implementación reutiliza el valor almacenado inmediatamente.

Una vez completado el barrido de fronteras, la misma recurrencia se aplica a \(N=10^{17}\). La versión en C++ además compara varios casos pequeños con fuerza bruta para verificar que la formulación rápida coincide con la simulación directa.

Análisis de Complejidad

Sea \(q=\lfloor\sqrt[3]{N-1}\rfloor\) y sea \(M\) el número de argumentos distintos que realmente se memorizan. El preprocesamiento determinista es el barrido lineal sobre las \(q\) fronteras cúbicas, así que esa parte cuesta \(O(q)\).

Para un argumento no cacheado \(x\), un paso de la recurrencia lo reemplaza por un residuo \(r\) de tamaño \(O(x^{2/3})\). En consecuencia, una cadena recursiva individual tiene profundidad \(O(\log\log N)\). Como cada estado se resuelve una sola vez, el tiempo total es \(O(M)\) evaluaciones de raíz cúbica más \(O(q)\) actualizaciones de frontera, y la memoria es \(O(M+q)\). En la práctica \(M\) se mantiene cerca de la escala cúbica y es diminuto frente a \(N\).

Notas y Referencias

  1. Project Euler problem page: https://projecteuler.net/problem=884
  2. Número cúbico: Wikipedia — Número cúbico
  3. Algoritmo voraz: Wikipedia — Algoritmo voraz
  4. Memoización: Wikipedia — Memoización
  5. Programación dinámica: Wikipedia — Programación dinámica

问题概述

设 \(d(n)\) 表示把 \(n\) 通过如下贪心规则降到 \(0\) 所需的步数:每一步都减去不超过当前值的最大完全立方数。题目要求计算

$$T(N)=\sum_{n=1}^{N-1} d(n),$$

其中最终目标是 \(N=10^{17}\)。如果对每个 \(n<N\) 单独模拟,代价会大得不可接受,所以关键是把许多整数按“第一次减去哪个立方数”分组,再对整组一起求和。

数学方法

为了让递推写得更自然,我们先约定 \(d(0)=0\),并定义前缀求和函数

$$T(x)=\sum_{n=1}^{x-1} d(n)\qquad (x\ge 1).$$

这样一来,题目的答案就是 \(T(10^{17})\)。

Step 1: 按第一次减去的立方数分块

任取 \(x>1\),令

$$m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\qquad b=m^3,\qquad r=x-b.$$

因为 \(b\le x-1 < (m+1)^3\),所以区间

$$b,\ b+1,\ \dots,\ x-1$$

中的每个整数,第一次贪心操作都会减去同一个立方数 \(b=m^3\)。

区间里的任意数都可写成 \(b+u\),其中 \(0\le u<r\)。它的第一步一定是

$$b+u \longrightarrow u,$$

因此有

$$d(b+u)=1+d(u).$$

这一步把“从 \(b+u\) 开始的问题”拆成了“一次固定操作”加上“从较小余数 \(u\) 开始的问题”。

Step 2: 把整块求和化成核心递推

把上式对 \(u=0,1,\dots,r-1\) 求和,就得到

$$T(x)-T(b)=\sum_{u=0}^{r-1}\bigl(1+d(u)\bigr)=r+T(r).$$

现在定义立方边界上的总和

$$B_m=T(m^3).$$

于是主递推写成

$$\boxed{T(x)=B_m+r+T(r),\qquad m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\ r=x-m^3.}$$

这个公式的意义非常直接:先加上所有小于 \(m^3\) 的贡献,再加上当前块里每个数都会多出来的那一次减法,最后再补上这些余数本身继续递归时产生的总贡献。

Step 3: 在立方边界上得到更简单的递推

如果把 \(x\) 取成下一个整立方,即 \(x=(m+1)^3\),那么余数恰好是相邻两个立方之间的间隔

$$r=(m+1)^3-m^3=3m^2+3m+1.$$

代回主递推可得

$$B_{m+1}=B_m+\bigl((m+1)^3-m^3\bigr)+T\bigl((m+1)^3-m^3\bigr).$$

这说明只要按 \(m=1,2,3,\dots\) 的顺序向前推进,就能逐个算出所有立方边界值。每前进一步,需要处理的只是一个远小于 \((m+1)^3\) 的差值。

Step 4: 为什么递归参数会迅速变小

由定义可知余数满足

$$1\le r\le (m+1)^3-m^3=3m^2+3m+1.$$

而 \(m\) 大约是 \(x^{1/3}\),因此得到粗略上界

$$r\le 3x^{2/3}+3x^{1/3}+1.$$

也就是说,一次递归会把规模大约为 \(x\) 的问题降到规模大约为 \(x^{2/3}\) 的问题。继续递归时,指数会从 \(1\) 变成 \(2/3\)、再变成 \((2/3)^2\)、\((2/3)^3\) 等,很快就降到很小的范围。这正是为什么题目虽然目标是 \(10^{17}\),但状态数量仍然非常可控。

Step 5: 用记忆化把递归变成动态规划

$$q=\left\lfloor\sqrt[3]{N-1}\right\rfloor.$$

算法首先从左到右计算所有边界值

$$B_1,\ B_2,\ \dots,\ B_q.$$

如果在这个过程中需要某个非边界位置的 \(T(x)\),就继续使用同样的公式

$$T(x)=B_m+r+T(r)$$

并把求出的结果缓存起来。由于不同立方间隔和递归余数之间会出现大量重叠,一旦某个状态已经算过,后面再次遇到时就可以直接复用。这样,原本可能分叉的递归树会被压缩成一个只含已知状态的有向无环图。

Worked Example

看一个小例子:取 \(x=10\)。此时

$$m=\left\lfloor\sqrt[3]{9}\right\rfloor=2,\qquad b=2^3=8,\qquad r=10-8=2.$$

主递推给出

$$T(10)=T(8)+2+T(2).$$

$$T(8)=d(1)+d(2)+\cdots+d(7)=1+2+3+4+5+6+7=28,$$

并且 \(T(2)=d(1)=1\)。所以

$$T(10)=28+2+1=31.$$

这和直接观察完全一致:\(8\) 只需减一次,\(9\) 先减 \(8\) 再减 \(1\),所以最后两个数额外贡献了 \(1+2=3\)。

代码如何工作

C++、Python 和 Java 三个实现采用的是同一套思路。首先,它们都会先用浮点立方根得到一个初值,然后通过比较相邻立方数向上或向下微调,最终得到精确的整数下取整立方根。这样可以避免在完美立方附近因为浮点误差而选错块。

接着,程序建立一个数组,用来保存所有立方边界上的总和 \(B_m=T(m^3)\)。这些值按顺序推进,每次只需要知道相邻两个立方之间的差,以及这个差值对应的递归总和。

所有已经求出的 \(T(x)\) 都会被放入哈希表或字典中。之后如果同一个余数再次出现,程序就直接返回缓存值,不再重复展开整条递归链。

当所有边界值都准备好以后,程序再把同样的递推应用到最终目标 \(N=10^{17}\) 上。C++ 实现还额外保留了一些小范围的暴力校验,用来确认快速方法和直接模拟在小样例上完全一致。

复杂度分析

设 \(q=\lfloor\sqrt[3]{N-1}\rfloor\),再设 \(M\) 为记忆化过程中真正出现并被缓存的不同参数个数。确定性的预处理就是从左到右扫过这 \(q\) 个立方边界,因此这一部分是 \(O(q)\)。

对任意一个尚未缓存的参数 \(x\),一次递推会把它变成大小为 \(O(x^{2/3})\) 的余数,所以单条递归链的深度是 \(O(\log\log N)\)。由于每个状态最多只求一次,总时间可以写成 \(O(M)\) 次立方根计算再加上 \(O(q)\) 次边界更新,而空间复杂度是 \(O(M+q)\)。实际运行时,\(M\) 会停留在立方根量级,和 \(N\) 相比非常小。

脚注与参考资料

  1. Project Euler problem page: https://projecteuler.net/problem=884
  2. 立方数: Wikipedia — 立方数
  3. 贪心算法: Wikipedia — 贪心算法
  4. 记忆化: Wikipedia — 记忆化
  5. 动态规划: Wikipedia — 动态规划

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

Пусть \(d(n)\) обозначает число жадных шагов, необходимых для сведения \(n\) к \(0\), если на каждом шаге вычитать наибольший полный куб, не превосходящий текущее значение. Требуется вычислить

$$T(N)=\sum_{n=1}^{N-1} d(n),$$

где в задаче нужно взять \(N=10^{17}\). Полная симуляция для всех \(n<N\) невозможна, поэтому решение группирует числа по тому, какой куб снимается первым, и превращает огромную сумму в сравнительно компактную рекурсивную динамику.

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

Для удобства положим \(d(0)=0\) и введем функцию частичных сумм

$$T(x)=\sum_{n=1}^{x-1} d(n)\qquad (x\ge 1).$$

Тогда искомый ответ есть \(T(10^{17})\).

Step 1: Разбиение по первому удаляемому кубу

Возьмем произвольное \(x>1\) и положим

$$m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\qquad b=m^3,\qquad r=x-b.$$

Из неравенства \(b\le x-1 < (m+1)^3\) следует, что каждое число из диапазона

$$b,\ b+1,\ \dots,\ x-1$$

в первом жадном шаге вычитает один и тот же куб \(b=m^3\).

Любое такое число имеет вид \(b+u\), где \(0\le u<r\), и его начало разложения таково:

$$b+u \longrightarrow u.$$

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

$$d(b+u)=1+d(u).$$

Step 2: Превращаем блочную формулу в рекуррентное соотношение

Суммируя предыдущее тождество по \(u=0,1,\dots,r-1\), получаем

$$T(x)-T(b)=\sum_{u=0}^{r-1}\bigl(1+d(u)\bigr)=r+T(r).$$

Введем теперь значения на кубических границах

$$B_m=T(m^3).$$

Тогда основная рекурсия принимает вид

$$\boxed{T(x)=B_m+r+T(r),\qquad m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\ r=x-m^3.}$$

Именно эта формула лежит в основе всех трех реализаций.

Step 3: Специальный переход на точных кубических границах

Если выбрать \(x=(m+1)^3\), то остаток равен

$$r=(m+1)^3-m^3=3m^2+3m+1.$$

Подстановка в основную рекурсию дает

$$B_{m+1}=B_m+\bigl((m+1)^3-m^3\bigr)+T\bigl((m+1)^3-m^3\bigr).$$

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

Step 4: Почему остаток быстро уменьшается

Для остатка выполняется оценка

$$1\le r\le (m+1)^3-m^3=3m^2+3m+1.$$

Так как \(m\) имеет порядок \(x^{1/3}\), получаем грубую верхнюю границу

$$r\le 3x^{2/3}+3x^{1/3}+1.$$

Значит, один рекурсивный шаг уменьшает аргумент примерно с масштаба \(x\) до масштаба \(x^{2/3}\). После нескольких повторений размер становится очень маленьким. Именно поэтому мемоизация делает метод практичным даже при \(N=10^{17}\).

Step 5: Мемоизация превращает рекурсию в динамическое программирование

Пусть

$$q=\left\lfloor\sqrt[3]{N-1}\right\rfloor.$$

Сначала алгоритм последовательно вычисляет все граничные значения

$$B_1,\ B_2,\ \dots,\ B_q.$$

Если по пути требуется значение \(T(x)\) для точки, не являющейся кубической границей, снова применяется формула

$$T(x)=B_m+r+T(r),$$

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

Worked Example

Рассмотрим \(x=10\). Тогда

$$m=\left\lfloor\sqrt[3]{9}\right\rfloor=2,\qquad b=2^3=8,\qquad r=10-8=2.$$

Основная формула дает

$$T(10)=T(8)+2+T(2).$$

Здесь \(T(8)=d(1)+\cdots+d(7)=1+2+3+4+5+6+7=28\), а \(T(2)=d(1)=1\). Следовательно,

$$T(10)=28+2+1=31.$$

Это полностью совпадает с прямым подсчетом: \(d(8)=1\), а \(d(9)=2\), значит два последних числа добавляют ровно \(3\) к сумме до \(8\).

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

Реализации на C++, Python и Java устроены одинаково. Сначала они получают точный целочисленный кубический корень: берут плавающее приближение, а затем подправляют его вверх или вниз, пока соседние кубы не окажутся по правильные стороны от аргумента. Это снимает риск ошибок на границах полных кубов.

Затем строится массив граничных сумм \(B_m=T(m^3)\) вплоть до \(m=\lfloor\sqrt[3]{N-1}\rfloor\). Каждый новый элемент вычисляется по граничной рекурсии, поэтому на шаге нужны только разность соседних кубов и одно рекурсивное значение для этой разности.

Все уже найденные значения \(T(x)\) записываются в хеш-таблицу. Если позже тот же остаток встретится снова, программа просто вернет сохраненный результат.

После завершения прохода по границам та же рекурсия применяется к конечной цели \(N=10^{17}\). В C++-версии добавлены и маленькие проверки грубой силой на небольших входах, чтобы сверить быструю схему с прямой симуляцией.

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

Пусть \(q=\lfloor\sqrt[3]{N-1}\rfloor\), а \(M\) обозначает число различных аргументов, реально попавших в кэш. Полностью определенная часть алгоритма, то есть проход по \(q\) кубическим границам, стоит \(O(q)\).

Для любого невычисленного ранее аргумента \(x\) один шаг рекурсии заменяет его остатком размера \(O(x^{2/3})\), поэтому глубина отдельной цепочки составляет \(O(\log\log N)\). Так как каждое уникальное состояние решается только один раз, общая трудоемкость равна \(O(M)\) вычислений кубического корня плюс \(O(q)\) граничных обновлений, а память равна \(O(M+q)\). На практике \(M\) остается порядка кубического корня и ничтожно мало по сравнению с \(N\).

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

  1. Project Euler problem page: https://projecteuler.net/problem=884
  2. Кубическое число: Wikipedia — Кубическое число
  3. Жадный алгоритм: Wikipedia — Жадный алгоритм
  4. Мемоизация: Wikipedia — Мемоизация
  5. Динамическое программирование: Wikipedia — Динамическое программирование

ملخص المسألة

لتكن \(d(n)\) هي عدد الخطوات الجشعة اللازمة لاختزال \(n\) إلى \(0\) عندما نطرح في كل مرة أكبر مكعب كامل لا يتجاوز القيمة الحالية. المطلوب في المسألة هو حساب

$$T(N)=\sum_{n=1}^{N-1} d(n),$$

مع الهدف النهائي \(N=10^{17}\). من المستحيل عمليًا محاكاة جميع القيم \(n<N\) واحدةً واحدة، لذلك تجمع الفكرة الرياضية الأعداد بحسب المكعب الذي يُطرح في الخطوة الأولى، ثم تحوّل الجمع الضخم إلى برنامج ديناميكي استدعائي صغير نسبيًا.

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

لتسهيل الصياغة نضع اصطلاحًا \(d(0)=0\)، ثم نعرّف دالة المجاميع الجزئية

$$T(x)=\sum_{n=1}^{x-1} d(n)\qquad (x\ge 1).$$

وعندئذ تصبح الإجابة المطلوبة هي \(T(10^{17})\).

Step 1: تقسيم الأعداد بحسب أول مكعب يُطرح

خذ أي \(x>1\)، وعرّف

$$m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\qquad b=m^3,\qquad r=x-b.$$

بما أن \(b\le x-1 < (m+1)^3\)، فإن كل عدد في المجال

$$b,\ b+1,\ \dots,\ x-1$$

يبدأ بالخطوة الجشعة نفسها تمامًا: طرح \(b=m^3\).

ويمكن كتابة أي عدد في هذه الكتلة على الصورة \(b+u\) حيث \(0\le u<r\)، فتبدأ عمليته بـ

$$b+u \longrightarrow u.$$

ومن ثم

$$d(b+u)=1+d(u).$$

Step 2: استخراج علاقة جمع على مستوى الكتلة كلها

إذا جمعنا العلاقة السابقة على \(u=0,1,\dots,r-1\)، نحصل على

$$T(x)-T(b)=\sum_{u=0}^{r-1}\bigl(1+d(u)\bigr)=r+T(r).$$

ولنعرّف الآن قيم الحدود المكعبة بواسطة

$$B_m=T(m^3).$$

فتصبح العلاقة الأساسية

$$\boxed{T(x)=B_m+r+T(r),\qquad m=\left\lfloor\sqrt[3]{x-1}\right\rfloor,\ r=x-m^3.}$$

وهذه هي الصيغة الجوهرية التي تعتمد عليها جميع التطبيقات.

Step 3: علاقة خاصة عند حدود المكعبات

إذا أخذنا \(x=(m+1)^3\)، فإن الباقي يساوي الفجوة بين مكعبين متتاليين:

$$r=(m+1)^3-m^3=3m^2+3m+1.$$

وبالتعويض في العلاقة الأساسية نحصل على

$$B_{m+1}=B_m+\bigl((m+1)^3-m^3\bigr)+T\bigl((m+1)^3-m^3\bigr).$$

وهذا يعني أن قيم الحدود \(B_1,B_2,\dots\) يمكن بناؤها من اليسار إلى اليمين، لأن كل حد جديد يحتاج فقط إلى وسيط أصغر بكثير من المكعب الجديد نفسه.

Step 4: لماذا يصغر الباقي بسرعة كبيرة

لدينا دائمًا

$$1\le r\le (m+1)^3-m^3=3m^2+3m+1.$$

وبما أن \(m\) من رتبة \(x^{1/3}\)، ينتج التقدير التقريبي

$$r\le 3x^{2/3}+3x^{1/3}+1.$$

أي إن خطوة استدعائية واحدة تخفّض وسيطًا بحجم يقارب \(x\) إلى وسيط بحجم يقارب \(x^{2/3}\). وبعد تكرار العملية عدة مرات يصبح الحجم صغيرًا جدًا، ولهذا تكفي الذاكرة المؤقتة لجعل الحساب عمليًا حتى عند \(10^{17}\).

Step 5: تحويل العلاقة إلى برنامج ديناميكي مع حفظ النتائج

لنكتب

$$q=\left\lfloor\sqrt[3]{N-1}\right\rfloor.$$

تبدأ الخوارزمية بحساب جميع قيم الحدود

$$B_1,\ B_2,\ \dots,\ B_q$$

بالترتيب. وعندما تحتاج إلى قيمة \(T(x)\) لا تقع على حد مكعّبي، فإنها تعيد تطبيق العلاقة

$$T(x)=B_m+r+T(r)$$

ثم تحفظ النتيجة. وبسبب تكرار كثير من الفجوات بين المكعبات وكثير من البواقي، فإن التخزين المؤقت يضغط شجرة الاستدعاءات إلى رسم بياني لا دوري من حالات سبق حلّها.

Worked Example

لنأخذ \(x=10\). عندئذ

$$m=\left\lfloor\sqrt[3]{9}\right\rfloor=2,\qquad b=2^3=8,\qquad r=10-8=2.$$

وتعطينا العلاقة الأساسية

$$T(10)=T(8)+2+T(2).$$

نحسب أن \(T(8)=d(1)+\cdots+d(7)=1+2+3+4+5+6+7=28\)، وكذلك \(T(2)=d(1)=1\). إذن

$$T(10)=28+2+1=31.$$

وهذا يطابق العد المباشر تمامًا: فالقيمة \(8\) تحتاج إلى خطوة واحدة، والقيمة \(9\) تحتاج إلى خطوتين، لذا تضيف آخر قيمتين مقدار \(3\) فوق المجموع حتى \(8\).

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

تتبع تطبيقات C++ وPython وJava البنية نفسها. أولًا تُحسب الجذر التكعيبي الصحيح بالانطلاق من تقدير عشري، ثم تُصحَّح القيمة صعودًا أو هبوطًا حتى تقع المكعبات المجاورة في الموضع الصحيح. بهذه الطريقة لا تبقى أخطاء تقريب قرب المكعبات الكاملة.

بعد ذلك تُبنى مصفوفة لقيم الحدود المكعبة \(B_m=T(m^3)\) حتى \(m=\lfloor\sqrt[3]{N-1}\rfloor\). وكل حد جديد يُحسب باستعمال علاقة الحدود، أي من فرق مكعبين متتاليين ومن قيمة مجموع جزئي واحدة مرتبطة بهذا الفرق.

كل قيمة محسوبة سابقًا من \(T(x)\) تُخزَّن في قاموس أو جدول تجزئة. فإذا ظهر الباقي نفسه مرة أخرى، تسترجع الخوارزمية النتيجة المحفوظة فورًا بدل إعادة الحساب.

بعد اكتمال المرور على الحدود، تُطبَّق العلاقة نفسها مرة أخيرة على \(N=10^{17}\). كما تتضمن نسخة C++ اختبارات صغيرة بالمحاكاة المباشرة للتأكد من أن الطريقة السريعة تطابق العد الخام على المدخلات الصغيرة.

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

ليكن \(q=\lfloor\sqrt[3]{N-1}\rfloor\)، وليكن \(M\) عدد الوسائط المختلفة التي تُحفظ فعليًا في الذاكرة المؤقتة. الجزء الحتمي من العمل هو المرور الأمامي على حدود المكعبات وعددها \(q\)، ولذلك تكلفته \(O(q)\).

أما لأي وسيط غير مخزَّن \(x\)، فإن خطوة واحدة من العلاقة تستبدله بباقٍ حجمه \(O(x^{2/3})\)، ولذلك يكون عمق سلسلة الاستدعاء الواحدة \(O(\log\log N)\). وبما أن كل حالة مميزة تُحل مرة واحدة فقط، فالزمن الكلي هو \(O(M)\) عمليات جذر تكعيبي بالإضافة إلى \(O(q)\) تحديثات حدودية، في حين أن الذاكرة المستعملة هي \(O(M+q)\). وعمليًا يبقى \(M\) قريبًا من مقياس الجذر التكعيبي، أي صغيرًا جدًا مقارنةً بـ \(N\).

هوامش ومراجع

  1. Project Euler problem page: https://projecteuler.net/problem=884
  2. العدد المكعّب: Wikipedia — العدد المكعّب
  3. الخوارزمية الجشعة: Wikipedia — الخوارزمية الجشعة
  4. Memoization: Wikipedia — Memoization
  5. البرمجة الديناميكية: Wikipedia — البرمجة الديناميكية