Problem 383: Divisibility Comparison Between Factorials
View on Project EulerProject Euler Problem 383 Solution
EulerSolve provides an optimized solution for Project Euler Problem 383, Divisibility Comparison Between Factorials, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary Define $T_5(N)=\#\left\{1\le i\le N: v_5((2i-1)!) \lt 2v_5(i!)\right\}.$ We must evaluate \(T_5(10^{18})\). A direct scan over all \(i\) is impossible, so the implemented solution turns the factorial inequality into a statement about carries in base \(5\), then counts the valid numbers with a memoized digit DP. Mathematical Approach Step 1: Rewrite the factorial valuation Because \((2i)! = 2i\,(2i-1)!\), we have $v_5((2i-1)!) = v_5((2i)!)-v_5(2i).$ Subtracting \(2v_5(i!)\) gives $v_5((2i-1)!)-2v_5(i!)=v_5((2i)!)-2v_5(i!)-v_5(2i).$ The middle part is exactly the \(5\)-adic valuation of the central binomial coefficient: $v_5((2i)!)-2v_5(i!)=v_5\left(\binom{2i}{i}\right).$ Since \(v_5(2)=0\), we also have \(v_5(2i)=v_5(i)\). Therefore the original condition is equivalent to $v_5\left(\binom{2i}{i}\right) \lt v_5(i).$ Legendre's formula explains why factorial valuations are natural here, but the code uses the binomial rewrite because it exposes a carry-counting interpretation immediately. Step 2: Apply Kummer's theorem in base \(5\) Kummer's theorem states that \(v_5\left(\binom{2i}{i}\right)\) equals the number of carries when adding \(i+i\) in base \(5\). Write $i=5^a m,\qquad a=v_5(i),\qquad 5\nmid m.$ Multiplying by \(5^a\) merely appends \(a\) trailing zeros in base \(5\), so it does not create or destroy carries; it only shifts their positions....
Detailed mathematical approach
Problem Summary
Define
$T_5(N)=\#\left\{1\le i\le N: v_5((2i-1)!) \lt 2v_5(i!)\right\}.$
We must evaluate \(T_5(10^{18})\). A direct scan over all \(i\) is impossible, so the implemented solution turns the factorial inequality into a statement about carries in base \(5\), then counts the valid numbers with a memoized digit DP.
Mathematical Approach
Step 1: Rewrite the factorial valuation
Because \((2i)! = 2i\,(2i-1)!\), we have
$v_5((2i-1)!) = v_5((2i)!)-v_5(2i).$
Subtracting \(2v_5(i!)\) gives
$v_5((2i-1)!)-2v_5(i!)=v_5((2i)!)-2v_5(i!)-v_5(2i).$
The middle part is exactly the \(5\)-adic valuation of the central binomial coefficient:
$v_5((2i)!)-2v_5(i!)=v_5\left(\binom{2i}{i}\right).$
Since \(v_5(2)=0\), we also have \(v_5(2i)=v_5(i)\). Therefore the original condition is equivalent to
$v_5\left(\binom{2i}{i}\right) \lt v_5(i).$
Legendre's formula explains why factorial valuations are natural here, but the code uses the binomial rewrite because it exposes a carry-counting interpretation immediately.
Step 2: Apply Kummer's theorem in base \(5\)
Kummer's theorem states that \(v_5\left(\binom{2i}{i}\right)\) equals the number of carries when adding \(i+i\) in base \(5\). Write
$i=5^a m,\qquad a=v_5(i),\qquad 5\nmid m.$
Multiplying by \(5^a\) merely appends \(a\) trailing zeros in base \(5\), so it does not create or destroy carries; it only shifts their positions. Hence
$\operatorname{carries}_5(i+i)=\operatorname{carries}_5(m+m).$
The inequality becomes
$\operatorname{carries}_5(m+m) \lt a,$
and because the carry count is an integer, this is the same as
$\operatorname{carries}_5(m+m)\le a-1.$
Step 3: Sum over the \(5\)-adic order of \(i\)
Every positive integer \(i\) has a unique decomposition \(i=5^a m\) with \(5\nmid m\). For a fixed \(a\ge 1\), the admissible values of \(m\) satisfy
$1\le m\le \left\lfloor\frac{N}{5^a}\right\rfloor,\qquad 5\nmid m,$
and they contribute exactly when \(\operatorname{carries}_5(m+m)\le a-1\). Define \(G(x,k)\) to be the number of positive integers \(m\le x\), not divisible by \(5\), whose doubling in base \(5\) produces at most \(k\) carries. Then
$\boxed{T_5(N)=\sum_{a\ge 1} G\left(\left\lfloor\frac{N}{5^a}\right\rfloor,a-1\right).}$
The sum stops as soon as \(5^a \gt N\). For \(N=10^{18}\), only \(25\) values of \(a\) need to be examined.
Step 4: Derive the least-significant-digit DP
The programs process digits from least significant to most significant. Let \(F(x,k,c)\) denote the number of integers \(0\le n\le x\) such that, when doubling \(n\) in base \(5\), the still-unprocessed higher digits create at most \(k\) additional carries, assuming an incoming carry \(c\in\{0,1\}\) from the lower digits.
If the current digit is \(d\in\{0,1,2,3,4\}\), the outgoing carry is
$c'=\left\lfloor\frac{2d+c}{5}\right\rfloor.$
Writing \(n=d+5q\) gives \(q\le \left\lfloor\frac{x-d}{5}\right\rfloor\), so the recurrence is
$F(x,k,c)=\sum_{\substack{0\le d\le 4\\ d\le x}} F\left(\left\lfloor\frac{x-d}{5}\right\rfloor,k-c',c'\right).$
The base cases are exactly the ones in the source files:
$F(x,k,c)=0\quad \text{if }x\lt 0\text{ or }k\lt 0,\qquad F(0,k,c)=1\quad \text{for }k\ge 0.$
To enforce \(5\nmid m\), the least significant digit must be \(1,2,3,\) or \(4\). Therefore
$G(x,k)=\sum_{\substack{1\le d\le 4\\ d\le x}} F\left(\left\lfloor\frac{x-d}{5}\right\rfloor,k-\left\lfloor\frac{2d}{5}\right\rfloor,\left\lfloor\frac{2d}{5}\right\rfloor\right).$
This is precisely what the helper count_non_multiples_of_five computes.
Step 5: Small examples
If \(i=20=5\cdot 4\), then \(a=1\) and \(m=4\). In base \(5\),
$\left(4\right)_5+\left(4\right)_5=\left(13\right)_5,$
so there is one carry. Since \(1 \nleq 0\), this \(i\) does not contribute. By contrast, \(i=35=5\cdot 7\) has \(m=\left(12\right)_5\), and
$\left(12\right)_5+\left(12\right)_5=\left(24\right)_5,$
which creates no carries, so it does contribute for \(a=1\). This is exactly the local criterion used by the DP.
How the Code Works
The C++, Python, and Java files all implement the same recurrence. carry_out returns \(c'=\lfloor(2d+c)/5\rfloor\). count_with_initial_carry is the memoized function \(F(x,k,c)\), keyed by \((x,k,\text{carry\_in})\). count_non_multiples_of_five handles the first digit separately so the counted number is positive and not divisible by \(5\). The outer loop iterates over \(a=1,2,\dots\) while \(5^a\le N\), accumulating \(G(\lfloor N/5^a\rfloor,a-1)\). The C++ implementation also validates the method with checkpoints \(T_5(10^3)=68\), \(T_5(10^9)=2408210\), and a brute-force comparison at \(N=20000\).
Complexity Analysis
Let \(A=\lfloor\log_5 N\rfloor\). The outer sum has \(A\) terms, and each recursive step removes one base-\(5\) digit. Every memo state has only \(5\) outgoing transitions and is determined by a triple \((x,k,c)\) with \(c\in\{0,1\}\). Thus the cost is proportional to the number of reachable memo states rather than to \(N\) itself, and the memory usage is the same order as the memo table. For the actual target \(N=10^{18}\), the reachable state space is tiny, so the method is effectively instantaneous compared with brute force.
References
- Problem page: https://projecteuler.net/problem=383
- Kummer's theorem: Wikipedia — Kummer's theorem
- Legendre's formula: Wikipedia — Legendre's formula
- \(p\)-adic valuation: Wikipedia — \(p\)-adic valuation