Number Theory & Algebra contents
Factorial Modulo a Prime
Compute n! mod p for a small prime p and huge n by Wilson's theorem, and find the exponent of p in n!.
Read first: Modular Multiplicative Inverse, Binomial Coefficients
For the factorial mod is a simple loop. For huge and a small prime (say , ) the loop is impossible, and moreover contains the factor many times, so the value modulo is just . The meaningful quantity is the factorial with all factors of removed:
where is the exponent of in . Computing it in makes binomial coefficients modulo a small prime possible (see Lucas' theorem) and answers many "trailing zeros" questions.
The exponent of in (Legendre's formula)
Multiples of contribute one factor, multiples of one extra, and so on.
def legendre(n, p):
e = 0
while n:
n //= p
e += n
return e
from math import factorial
assert legendre(100, 5) == 24 # 100! ends with 24 zeros
def naive_exp(n, p):
f, e = factorial(n), 0
while f % p == 0:
f //= p
e += 1
return e
assert all(legendre(n, p) == naive_exp(n, p) for n in range(0, 200) for p in (2, 3, 5, 7))The factorial without factors of
Split the numbers into multiples of and the rest.
The non-multiples. Every full block of consecutive numbers contains each non-zero residue once, so its non-multiples multiply to by Wilson's theorem. There are full blocks and then a partial block of numbers:
The multiples. They are ; dropping the factor from each leaves , itself a factorial, so recurse:
The recursion depth is , and each level needs , obtained from a table of factorials up to (one precomputation).
def factorial_mod_p(n, p):
"""(n! with all factors of p removed) mod p, and the exponent of p in n!."""
table = [1] * p
for i in range(1, p):
table[i] = table[i - 1] * i % p
result = 1
m = n
sign_parity = 0
while m > 0:
result = result * table[m % p] % p
m //= p
sign_parity += m # accumulates floor(n/p^k): parity gives (-1)^total
if sign_parity % 2 and p > 2:
result = (p - result) % p
return result, legendre(n, p)
def brute(n, p):
f = factorial(n)
e = 0
while f % p == 0:
f //= p
e += 1
return f % p, e
assert factorial_mod_p(10, 7) == brute(10, 7)
for p in (2, 3, 5, 7, 11, 13):
for n in range(0, 200):
assert factorial_mod_p(n, p) == brute(n, p), (n, p)The sign: each level contributes , so the total sign is , which is exactly sign_parity. For the sign does not matter since .
Binomial coefficients modulo small for huge
The formula lets us compute even when the binomial coefficient is divisible by (in which case the exponent difference is positive and the answer is ):
def binom_mod_p(n, k, p):
if k < 0 or k > n:
return 0
fn, en = factorial_mod_p(n, p)
fk, ek = factorial_mod_p(k, p)
fnk, enk = factorial_mod_p(n - k, p)
if en - ek - enk > 0:
return 0
return fn * pow(fk * fnk % p, -1, p) % p
from math import comb
for p in (5, 7, 13):
for n in range(0, 80):
for k in range(0, n + 1):
assert binom_mod_p(n, k, p) == comb(n, k) % pNote this recomputes the table for each call; in real use build table once and reuse it.
Complexity
for the table plus the recursion; when many queries share the same , the table is built once and each query costs .