Purpose: Multiply two polynomials with integer/modular coefficients in O(N log N) time, using modular arithmetic instead of complex roots of unity — avoiding FFT’s floating-point precision issues.
Algorithm
- Choose a prime modulus
pof the formp = c · 2^k + 1(an “NTT-friendly” prime, e.g.,998244353), which guarantees the existence of primitiveN-th roots of unity modulopforNup to2^k. - Find a primitive root
gofp, and computeω = g^((p-1)/N) mod pas the primitiveN-th root of unity to use (playing the same role ase^(2πi/N)in standard FFT). - Apply the exact same divide-and-conquer butterfly structure as FFT, but replacing complex arithmetic with modular arithmetic modulo
p, andωwith this modular root of unity. - Multiply the two transformed sequences pointwise modulo
p. - Apply the inverse NTT (using
ω⁻¹ mod p, scaled byN⁻¹ mod p) to recover the coefficient representation of the product, entirely within modular arithmetic — no rounding or floating-point error involved.
Code
const long long MOD = 998244353, ROOT = 3; // primitive root of MOD
long long powmod(long long a, long long e, long long m) {
long long r = 1; a %= m;
while (e) { if (e & 1) r = r * a % m; a = a * a % m; e >>= 1; }
return r;
}
void ntt(vector<long long>& a, bool invert) {
int n = a.size();
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1) j ^= bit;
j ^= bit;
if (i < j) swap(a[i], a[j]);
}
for (int len = 2; len <= n; len <<= 1) {
long long w = powmod(ROOT, (MOD - 1) / len, MOD);
if (invert) w = powmod(w, MOD - 2, MOD);
for (int i = 0; i < n; i += len) {
long long wn = 1;
for (int j = 0; j < len / 2; j++) {
long long u = a[i + j], v = a[i + j + len / 2] * wn % MOD;
a[i + j] = (u + v) % MOD;
a[i + j + len / 2] = (u - v + MOD) % MOD;
wn = wn * w % MOD;
}
}
}
if (invert) {
long long nInv = powmod(n, MOD - 2, MOD);
for (long long& x : a) x = x * nInv % MOD;
}
}
vector<long long> multiplyNTT(vector<long long> a, vector<long long> b) {
int n = 1;
while (n < (int)(a.size() + b.size())) n <<= 1;
a.resize(n); b.resize(n);
ntt(a, false); ntt(b, false);
for (int i = 0; i < n; i++) a[i] = a[i] * b[i] % MOD;
ntt(a, true);
return a;
}Paradigm
Divide and Conquer. Identical structural recursion to FFT (splitting into even/odd halves via a butterfly network), just carried out entirely within modular arithmetic instead of over the complex numbers.
Complexity
- Time: O(N log N)
- Space: O(N)
Proof of Correctness
Why a modular primitive N-th root of unity plays the same role as e^(2πi/N): The FFT’s correctness proof relies only on two algebraic properties of the roots of unity used: (1) ω^N = 1 and ω^(N/2) = -1 (needed for the butterfly’s A(ω^k) / A(-ω^k) symmetry), and (2) the orthogonality relation Σ ω^(jk) = 0 unless j ≡ 0 (mod N). A primitive N-th root of unity modulo a prime p (guaranteed to exist when N | (p-1), by the cyclic structure of (Z/pZ)*) satisfies exactly these same properties within modular arithmetic, so every step of the FFT correctness proof (the even/odd split, the butterfly combination, the interpolation/convolution argument) carries over verbatim, with mod p arithmetic replacing complex arithmetic throughout.
Why choosing p = c·2^k + 1 matters: This form guarantees (Z/pZ)* has order p - 1 = c · 2^k, which is divisible by any power of 2 up to 2^k — ensuring a primitive N-th root of unity exists for any N that’s a power of 2 up to 2^k, which is exactly what’s needed to run the standard power-of-2 FFT/NTT recursion without needing to pad to an inconveniently large or non-power-of-2 size.
Why NTT avoids FFT’s floating-point error: Since every operation (addition, multiplication, the roots of unity themselves) stays within exact modular integer arithmetic, there’s no rounding error accumulation — the final result is exact, whereas FFT with floating-point complex numbers accumulates small numerical errors that must be corrected via rounding at the end (fine for bounded-size integer coefficients, but can fail for very large coefficients or high precision requirements). ∎
Variants / Use Cases
- Convolution modulo a specific prime (competitive programming staple) → the primary use case, computing polynomial products modulo 998244353 or similar NTT-friendly primes
- Multiple-modulus NTT + CRT reconstruction → for convolutions requiring results modulo a non-NTT-friendly number, compute modulo several NTT-friendly primes and combine via CRT
- Fast big-integer multiplication (exact, no floating point) → NTT-based multiplication is used in some big-integer libraries for guaranteed-exact results
- Polynomial exponentiation, division, and other operations built on fast multiplication → NTT-based convolution is a building block for a wide range of polynomial algorithms
- Cryptographic applications (lattice-based cryptography, homomorphic encryption) → NTT is heavily used for fast polynomial ring arithmetic in these schemes