Problem 592: Factorial Trailing Digits 2
View on Project EulerProject Euler Problem 592 Solution
EulerSolve provides an optimized solution for Project Euler Problem 592, Factorial Trailing Digits 2, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary Let \(f(N)\) be the last 12 hexadecimal digits that remain after deleting all trailing hexadecimal zeroes from \(N!\). The problem asks for \(f(20!)\). A direct attack on \((20!)!\) is hopeless. The quantity is far too large to build explicitly, so the solution keeps only the information that survives after removing factors of \(16\) and reducing to 12 hexadecimal digits, namely arithmetic modulo \(16^{12}=2^{48}\). Mathematical Approach The key observation is that hexadecimal trailing zeroes depend only on powers of \(2\), because \(16=2^4\). Everything else belongs to the odd part of the factorial and can be handled modulo \(2^{48}\). Step 1: Separate the trailing hexadecimal zeroes For any positive integer \(m\), write $m=2^{\nu_2(m)}\operatorname{odd}(m),$ where \(\nu_2(m)\) is the exponent of \(2\) in \(m\), and \(\operatorname{odd}(m)\) is the odd part of \(m\)....
Detailed mathematical approach
Problem Summary
Let \(f(N)\) be the last 12 hexadecimal digits that remain after deleting all trailing hexadecimal zeroes from \(N!\). The problem asks for \(f(20!)\).
A direct attack on \((20!)!\) is hopeless. The quantity is far too large to build explicitly, so the solution keeps only the information that survives after removing factors of \(16\) and reducing to 12 hexadecimal digits, namely arithmetic modulo \(16^{12}=2^{48}\).
Mathematical Approach
The key observation is that hexadecimal trailing zeroes depend only on powers of \(2\), because \(16=2^4\). Everything else belongs to the odd part of the factorial and can be handled modulo \(2^{48}\).
Step 1: Separate the trailing hexadecimal zeroes
For any positive integer \(m\), write
$m=2^{\nu_2(m)}\operatorname{odd}(m),$
where \(\nu_2(m)\) is the exponent of \(2\) in \(m\), and \(\operatorname{odd}(m)\) is the odd part of \(m\).
Applied to \(N!\), this gives
$N!=2^{\nu_2(N!)}\operatorname{odd}(N!).$
Each trailing hexadecimal zero removes one factor \(16=2^4\), so after deleting all hexadecimal zeroes the remaining factor of \(2\) is only
$2^{\nu_2(N!) \bmod 4}.$
Therefore
$f(N)\equiv \operatorname{odd}(N!)\cdot 2^{\nu_2(N!) \bmod 4}\pmod{2^{48}}.$
The exponent \(\nu_2(N!)\) is computed by Legendre's formula:
$\nu_2(N!)=\sum_{k\ge 1}\left\lfloor\frac{N}{2^k}\right\rfloor.$
Step 2: Rewrite the odd part recursively
Define
$O(N)=\operatorname{odd}(N!),\qquad P(N)=\prod_{\substack{1\le m\le N\\ m\text{ odd}}} m.$
Split the factors of \(N!\) into odd numbers and even numbers. If a factor is \(2t\), then its odd part is just the odd part of \(t\). Hence
$O(N)=P(N)\,O\!\left(\left\lfloor\frac{N}{2}\right\rfloor\right).$
Iterating this identity yields
$O(N)=\prod_{j\ge 0} P\!\left(\left\lfloor\frac{N}{2^j}\right\rfloor\right),$
where the product stops once the floor becomes \(0\). So the hard part is reduced to evaluating the odd prefix product \(P(n)\) quickly for several values of \(n\).
Step 3: Group odd numbers by residue classes modulo \(2^{16}\)
To compute \(P(n)\) modulo \(2^{48}\), choose the block size
$S=2^{16}.$
Write
$n=qS+u,\qquad 0\le u<S.$
For each odd residue \(r\in\{1,3,5,\dots,S-1\}\), the odd numbers up to \(n\) in that class are
$r,\ r+S,\ r+2S,\ \dots,\ r+(q-1)S,$
and there is one additional term \(r+qS\) exactly when \(r\le u\).
So \(P(n)\) factors into a product of full class products and possibly one extra factor from each residue class:
$P(n)=\prod_{\substack{1\le r<S\\ r\text{ odd}}}\left(\prod_{j=0}^{q-1}(r+jS)\right)\cdot \prod_{\substack{1\le r\le u\\ r\text{ odd}}}(r+qS).$
Step 4: Truncate each class product after quadratic terms
Fix one odd residue \(r\), and define the full class product
$C_r(q)=\prod_{j=0}^{q-1}(r+jS).$
Because \(r\) is odd, it is invertible modulo \(2^{48}\). In the ring \(\mathbb{Z}/2^{48}\mathbb{Z}\), we can write
$C_r(q)=r^q\prod_{j=0}^{q-1}\left(1+j\,a\right),\qquad a=S\,r^{-1}.$
Now \(S=2^{16}\), so every factor \(a\) contains a factor \(2^{16}\). Any term of degree \(3\) or higher in the expansion of \(\prod(1+j\,a)\) contains \(a^3\), hence a factor \(2^{48}\), and therefore vanishes modulo \(2^{48}\).
Only the first two elementary symmetric sums survive:
$\prod_{j=0}^{q-1}(1+j\,a)\equiv 1+a\,e_1+a^2e_2\pmod{2^{48}},$
where
$e_1=\sum_{j=0}^{q-1}j=\frac{q(q-1)}{2},$
$e_2=\sum_{0\le i<j<q}ij=\frac{q(q-1)(q-2)(3q-1)}{24}.$
This replaces a long product by one modular power, one modular inverse, and a short polynomial evaluation.
Step 5: Reassemble the odd factorial part
For each level \(N,\lfloor N/2\rfloor,\lfloor N/4\rfloor,\dots\), compute the corresponding prefix product \(P(\cdot)\) using the residue-class formula above, and multiply all those values together. That gives \(O(N)=\operatorname{odd}(N!)\) modulo \(2^{48}\).
Finally restore the leftover factor of \(2\):
$f(N)\equiv O(N)\cdot 2^{\nu_2(N!) \bmod 4}\pmod{2^{48}}.$
Since the target is \(N=20!\), the implementations first compute \(20!\), then apply this fast routine to that value.
Worked Example: \(f(20)\)
The statement provides \(f(20)\) as a checkpoint, and it is a good small example of the method.
First,
$\nu_2(20!)=\left\lfloor\frac{20}{2}\right\rfloor+\left\lfloor\frac{20}{4}\right\rfloor+\left\lfloor\frac{20}{8}\right\rfloor+\left\lfloor\frac{20}{16}\right\rfloor=10+5+2+1=18.$
So after removing all hexadecimal zeroes, the remaining power of \(2\) is
$2^{18\bmod 4}=2^2=4.$
Next, the recursive identity gives
$O(20)=P(20)\,P(10)\,P(5)\,P(2)\,P(1).$
These small prefix products are
$P(20)=1\cdot 3\cdot 5\cdot 7\cdot 9\cdot 11\cdot 13\cdot 15\cdot 17\cdot 19=654729075,$
$P(10)=1\cdot 3\cdot 5\cdot 7\cdot 9=945,\qquad P(5)=1\cdot 3\cdot 5=15,\qquad P(2)=P(1)=1.$
Therefore
$O(20)=654729075\cdot 945\cdot 15=9280784638125.$
Reducing modulo \(2^{48}\) gives
$O(20)\equiv \texttt{0870D9DF20AD}_{16}\pmod{2^{48}}.$
Multiplying by the remaining factor \(4\) yields
$f(20)\equiv \texttt{21C3677C82B4}_{16},$
which matches the known checkpoint exactly.
How the Code Works
The C++, Python, and Java implementations all follow the same mathematics. They work entirely modulo \(2^{48}\), precompute modular inverses for every odd residue modulo \(2^{16}\), and use Newton iteration because odd numbers are invertible modulo powers of \(2\).
For the required input, they first build the integer \(20!\). Then they generate the sequence
$N,\ \left\lfloor\frac{N}{2}\right\rfloor,\ \left\lfloor\frac{N}{4}\right\rfloor,\ \dots$
until it reaches \(0\). For each level they compute the odd prefix product by scanning all odd residue classes modulo \(2^{16}\), using the closed form for the full blocks and multiplying one extra factor when the partial block reaches that residue.
Those level products are multiplied together to obtain the odd part of \(N!\). Separately, the exponent \(\nu_2(N!)\) is found by repeated halving. The final result restores the leftover factor \(2^{\nu_2(N!)\bmod 4}\) and formats the answer as a 12-digit uppercase hexadecimal string. The C++ implementation parallelizes the independent level computations, while the Python and Java implementations perform the same steps serially.
Complexity Analysis
The dominant work is evaluating the odd prefix product for the values \(N,\lfloor N/2\rfloor,\lfloor N/4\rfloor,\dots\), so there are \(O(\log N)\) levels. Each level scans the \(2^{15}\) odd residue classes modulo \(2^{16}\). Inside one class there is a modular exponentiation and a constant amount of fixed-width modular arithmetic; because the modulus is always \(2^{48}\) and the implementations operate on fixed-width integers, that inner cost stays small in practice.
In practical terms, the method behaves like a small constant times \(2^{15}\log N\), with \(O(2^{15})\) memory for the inverse table. The point is that the algorithm never builds \(N!\) itself; it only computes the residue information that can affect the final 12 hexadecimal digits.
Footnotes and References
- Problem page: https://projecteuler.net/problem=592
- Legendre's formula: Wikipedia - Legendre's formula
- \(p\)-adic valuation: Wikipedia - p-adic valuation
- Modular multiplicative inverse: Wikipedia - Modular multiplicative inverse
- Newton's method: Wikipedia - Newton's method