PyInfo
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!.

Advanced4 min readfactorialwilsonmodular arithmeticlegendre

Read first: Modular Multiplicative Inverse, Binomial Coefficients

For n<pn < p the factorial mod pp is a simple loop. For huge nn and a small prime pp (say n=1018n = 10^{18}, p=106+3p = 10^6 + 3) the loop is impossible, and moreover n!n! contains the factor pp many times, so the value modulo pp is just 00. The meaningful quantity is the factorial with all factors of pp removed:

n!p=n!pνp(n!) mod pn!_p = \frac{n!}{p^{\nu_p(n!)}} \bmod p

where νp(n!)\nu_p(n!) is the exponent of pp in n!n!. Computing it in O(plog⁡pn)O(p \log_p n) makes binomial coefficients modulo a small prime possible (see Lucas' theorem) and answers many "trailing zeros" questions.

The exponent of pp in n!n! (Legendre's formula)

νp(n!)=⌊np⌋+⌊np2⌋+⌊np3⌋+…\nu_p(n!) = \left\lfloor \frac{n}{p} \right\rfloor + \left\lfloor \frac{n}{p^2} \right\rfloor + \left\lfloor \frac{n}{p^3} \right\rfloor + \dots

Multiples of pp contribute one factor, multiples of p2p^2 one extra, and so on.

python
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 pp

Split the numbers 1,2,…,n1, 2, \dots, n into multiples of pp and the rest.

The non-multiples. Every full block of pp consecutive numbers contains each non-zero residue once, so its non-multiples multiply to (p−1)!≡−1(modp)(p-1)! \equiv -1 \pmod p by Wilson's theorem. There are ⌊n/p⌋\lfloor n/p \rfloor full blocks and then a partial block of n mod pn \bmod p numbers:

∏i≤np∤ii≡(−1)⌊n/p⌋⋅(n mod p)!(modp)\prod_{\substack{i \le n \\ p \nmid i}} i \equiv (-1)^{\lfloor n/p \rfloor} \cdot (n \bmod p)! \pmod p

The multiples. They are p,2p,…,⌊n/p⌋pp, 2p, \dots, \lfloor n/p \rfloor p; dropping the factor pp from each leaves ⌊n/p⌋!\lfloor n/p \rfloor !, itself a factorial, so recurse:

n!p=(−1)⌊n/p⌋⋅(n mod p)!⋅⌊np⌋!p(modp)n!_p = (-1)^{\lfloor n/p \rfloor} \cdot (n \bmod p)! \cdot \left\lfloor \frac{n}{p} \right\rfloor!_p \pmod p

The recursion depth is log⁡pn\log_p n, and each level needs (n mod p)!(n \bmod p)!, obtained from a table of factorials up to p−1p-1 (one O(p)O(p) precomputation).

python
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 (−1)⌊n/pk⌋(-1)^{\lfloor n/p^k \rfloor}, so the total sign is (−1)∑k⌊n/pk⌋(-1)^{\sum_k \lfloor n/p^k \rfloor}, which is exactly sign_parity. For p=2p = 2 the sign does not matter since −1≡1-1 \equiv 1.

Binomial coefficients modulo small pp for huge nn

The formula lets us compute (nk) mod p\binom{n}{k} \bmod p even when the binomial coefficient is divisible by pp (in which case the exponent difference is positive and the answer is 00):

python
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) % p

Note this recomputes the table for each call; in real use build table once and reuse it.

Complexity

O(p+log⁡pn)O(p + \log_p n) for the table plus the recursion; when many queries share the same pp, the table is built once and each query costs O(log⁡pn)O(\log_p n).

This article is a Python adaptation of “Factorial modulo p” from cp-algorithms.com, licensed under CC BY-SA 4.0. The text was condensed and rewritten and the C++ code was reimplemented in Python; this adaptation is shared under the same license.