Open Source · 7 Languages · 900+ Problems

Project Euler Solutions

Complete solutions in C++, Python & Java — with step-by-step mathematical explanations

All Problems

Problem 288: An Enormous Factorial

View on Project Euler

Project Euler Problem 288 Solution

EulerSolve provides an optimized solution for Project Euler Problem 288, An Enormous Factorial, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary A pseudo-random sequence produces digits \(T_n\in\{0,1,\dots,p-1\}\). These digits define a huge base-\(p\) integer $N=\sum_{n=0}^{q}T_n p^n.$ The required quantity is the \(p\)-adic valuation $\nu_p(N!)$ taken modulo \(p^e\). For the actual problem, \(p=61\), \(q=10^7\), \(e=10\), but the explanation below is written for general \(p,q,e\). Mathematical Approach 1) Start from Legendre’s formula. For every positive integer \(N\), $\nu_p(N!)=\sum_{k\ge 1}\left\lfloor\frac{N}{p^k}\right\rfloor.$ Since \(N\) has only \(q+1\) base-\(p\) digits, the sum actually stops at \(k=q\). 2) Expand each floor using the base-\(p\) digits. Because $N=\sum_{n=0}^{q}T_n p^n,$ dividing by \(p^k\) and taking the floor simply drops the lowest \(k\) digits. Therefore $\left\lfloor\frac{N}{p^k}\right\rfloor=\sum_{n=k}^{q}T_n p^{\,n-k}.$ This already shows why \(T_0\) never contributes to \(\nu_p(N!)\): every term in Legendre’s sum starts with division by at least one power of \(p\). 3) Swap the order of summation. Substitute the digit expansion into Legendre: $ \nu_p(N!)= \sum_{k=1}^{q}\sum_{n=k}^{q}T_n p^{\,n-k}. $ Now fix a digit position \(n\). It contributes once for every \(k=1,2,\dots,n\). Writing \(j=n-k\), we obtain $ \nu_p(N!)= \sum_{n=1}^{q} T_n \sum_{j=0}^{n-1} p^j....

Detailed mathematical approach

Problem Summary

A pseudo-random sequence produces digits \(T_n\in\{0,1,\dots,p-1\}\). These digits define a huge base-\(p\) integer

$N=\sum_{n=0}^{q}T_n p^n.$

The required quantity is the \(p\)-adic valuation

$\nu_p(N!)$

taken modulo \(p^e\). For the actual problem, \(p=61\), \(q=10^7\), \(e=10\), but the explanation below is written for general \(p,q,e\).

Mathematical Approach

1) Start from Legendre’s formula. For every positive integer \(N\),

$\nu_p(N!)=\sum_{k\ge 1}\left\lfloor\frac{N}{p^k}\right\rfloor.$

Since \(N\) has only \(q+1\) base-\(p\) digits, the sum actually stops at \(k=q\).

2) Expand each floor using the base-\(p\) digits. Because

$N=\sum_{n=0}^{q}T_n p^n,$

dividing by \(p^k\) and taking the floor simply drops the lowest \(k\) digits. Therefore

$\left\lfloor\frac{N}{p^k}\right\rfloor=\sum_{n=k}^{q}T_n p^{\,n-k}.$

This already shows why \(T_0\) never contributes to \(\nu_p(N!)\): every term in Legendre’s sum starts with division by at least one power of \(p\).

3) Swap the order of summation. Substitute the digit expansion into Legendre:

$ \nu_p(N!)= \sum_{k=1}^{q}\sum_{n=k}^{q}T_n p^{\,n-k}. $

Now fix a digit position \(n\). It contributes once for every \(k=1,2,\dots,n\). Writing \(j=n-k\), we obtain

$ \nu_p(N!)= \sum_{n=1}^{q} T_n \sum_{j=0}^{n-1} p^j. $

Equivalently, after swapping in the other direction,

$ \nu_p(N!)= \sum_{j\ge 0} p^j \sum_{n=j+1}^{q} T_n. $

This is the exact formula implemented by the code.

4) Why only the first \(e\) layers matter modulo \(p^e\). We only need the answer modulo \(p^e\). If \(j\ge e\), then \(p^j\) is already divisible by \(p^e\), so those terms vanish. Hence

$ \nu_p(N!)\equiv \sum_{j=0}^{e-1} p^j S_j \pmod{p^e}, \qquad S_j=\sum_{n=j+1}^{q} T_n. $

So the entire huge problem reduces to computing only \(e\) suffix sums of the digit sequence.

5) Why the code stores only a short prefix. Let

$T_{\mathrm{all}}=\sum_{n=0}^{q}T_n.$

Then each suffix can be written as

$ S_j=T_{\mathrm{all}}-\sum_{n=0}^{j}T_n. $

Therefore the solver only needs:

$\text{(a) the total sum of all digits, and}\qquad \text{(b) prefix sums up to index }e-1.$

This is why it stores

$\texttt{first\_len}=\min(e,q+1)$

prefix values, not the entire sequence.

6) Sequence generation. The digits are generated from

$s_{n+1}=s_n^2 \bmod 50515093,\qquad s_0=290797,$

and then

$T_n=s_n\bmod p.$

The algorithm streams the generator exactly once, accumulating the total digit sum and the first few prefix sums.

Worked Example

Take a toy base-\(5\) number with digits

$T_0=2,\qquad T_1=4,\qquad T_2=1.$

Then

$N=2+4\cdot 5+1\cdot 25=47.$

Legendre gives

$\nu_5(47!)=\left\lfloor\frac{47}{5}\right\rfloor+\left\lfloor\frac{47}{25}\right\rfloor=9+1=10.$

Now use the suffix formula:

$S_0=T_1+T_2=5,\qquad S_1=T_2=1.$

So

$\nu_5(47!)=5^0S_0+5^1S_1=5+5=10,$

exactly as expected.

Checks and Complexity

The source file includes the checkpoint

$\mathrm{NF}(3,10000)\bmod 3^{20}=624955285,$

and also compares the fast method against a brute-force big-integer computation for a smaller case.

Time complexity is

$O(q+e),$

because generating the digits costs \(O(q)\) and the final valuation sum costs \(O(e)\). Memory usage is

$O(e),$

since only a short prefix array and a few accumulators are stored.

Further Reading

  1. Problem page: https://projecteuler.net/problem=288
  2. Legendre’s formula: https://en.wikipedia.org/wiki/Legendre%27s_formula
  3. \(p\)-adic valuation: https://en.wikipedia.org/wiki/P-adic_valuation

Mathematical approach · C++ solution · Python solution · Java solution

Previous: Problem 287 · All Project Euler solutions · Next: Problem 289

Need help with a problem? Ask me! 💡
e
✦ Euler GLM 5.2
Hi! I'm Euler. Ask me anything about Project Euler problems, math concepts, or solution approaches! 🧮