The FFT over : for a prime , is cyclic of order , so it contains a primitive -th root of unity for every , and the Cooley–Tukey algorithm runs verbatim with exact integer arithmetic. This note’s block is the shared polynomial core of the chapter: ntt, convolution (templated on the Mint<P> of Modular Arithmetic), Poly = vector<mint> with polyMul, and the three-prime CRT convolution for an arbitrary modulus. Later notes (power series, evaluation, recurrences, counting) assume it.
| Prime | Form | Max length | Primitive root |
|---|---|---|---|
| 998244353 | 3 | ||
| 167772161 | 3 | ||
| 469762049 | 3 | ||
| 1004535809 | 3 | ||
| 754974721 | 11 | ||
| 2013265921 | 31 | ||
| 4179340454199820289 | 3 (needs 128-bit products) |
is not NTT-friendly (), which is why 998244353 is the standard contest modulus. For an arbitrary modulus : (1) three primes + CRT (below): the exact convolution value is for , , so CRT reconstructs it exactly; (2) coefficient splitting with the complex FFT (convMod in FFT); (3) __int128 accumulation for small .
constexpr ll powC(ll b, ll e, ll m) { ll r = 1; for (b %= m; e; e >>= 1, b = b * b % m) if (e & 1) r = r * b % m; return r; }
template <int P> constexpr int nonResidue() { int g = 2; while (powC(g, (P - 1) / 2, P) != P - 1) g++; return g; }
template <int P> void ntt(vector<Mint<P>>& a, bool invert = false) { // n a power of two dividing P - 1
using Z = Mint<P>; int n = sz(a); if (n <= 1) return;
static vector<Z> roots{0, 1}; // roots[k + j] = w_{2k}^j
if (sz(roots) < n) {
constexpr int g = nonResidue<P>(); int k = __builtin_ctz(sz(roots)); roots.resize(n);
for (; (1 << k) < n; k++) {
Z e = Z(g).pow((P - 1) >> (k + 1));
for (int i = 1 << (k - 1); i < (1 << k); i++) roots[2 * i] = roots[i], roots[2 * i + 1] = roots[i] * e;
}
}
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 k = 1; k < n; k *= 2)
for (int i = 0; i < n; i += 2 * k) for (int j = 0; j < k; j++) {
Z u = a[i + j], v = a[i + j + k] * roots[k + j];
a[i + j] = u + v; a[i + j + k] = u - v;
}
if (invert) { reverse(a.begin() + 1, a.end()); Z inv = Z(n).inv(); for (Z& x : a) x *= inv; }
}
template <int P> vector<Mint<P>> convolution(vector<Mint<P>> a, vector<Mint<P>> b) {
if (a.empty() || b.empty()) return {};
int need = sz(a) + sz(b) - 1;
if (min(sz(a), sz(b)) <= 32) { vector<Mint<P>> c(need); for (int i = 0; i < sz(a); i++) for (int j = 0; j < sz(b); j++) c[i + j] += a[i] * b[j]; return c; }
int n = 1; while (n < need) n *= 2;
a.resize(n); b.resize(n); ntt(a); ntt(b);
for (int i = 0; i < n; i++) a[i] *= b[i];
ntt(a, true); a.resize(need); return a;
}
using Poly = vector<mint>;
Poly polyMul(const Poly& a, const Poly& b) { return convolution(a, b); }
vector<ll> convolutionAnyMod(const vector<ll>& a, const vector<ll>& b, ll m) { // entries in [0, m), m <= ~1e9, n <= ~1e6
if (a.empty() || b.empty()) return {};
constexpr int P1 = 998244353, P2 = 167772161, P3 = 469762049;
auto run = [&](auto tag) { using Z = decltype(tag); vector<Z> x(all(a)), y(all(b)); return convolution(x, y); };
auto c1 = run(Mint<P1>()); auto c2 = run(Mint<P2>()); auto c3 = run(Mint<P3>());
const ll i12 = Mint<P2>(P1).inv().v, i123 = Mint<P3>((ll)P1 * P2 % P3).inv().v;
vector<ll> res(sz(c1));
for (int i = 0; i < sz(c1); i++) { // Garner: x = x1 + P1 t2 + P1 P2 t3
ll x1 = c1[i].v, t2 = (c2[i].v - x1 % P2 + P2) % P2 * i12 % P2;
ll x12 = x1 + (ll)P1 * t2; // < P1 P2 < 2^58
ll t3 = ((c3[i].v - x12 % P3) % P3 + P3) % P3 * i123 % P3;
res[i] = (ll)(((i128)x12 + (i128)P1 * P2 % m * t3) % m);
}
return res;
}Proof: the modular roots, correctness and the three-prime reconstruction
Roots. is cyclic of order . For a quadratic non-residue (Euler’s criterion: ) and , satisfies and , so its order is exactly : a primitive -th root of unity. The table construction keeps
roots[k + j]:roots[2i]roots[i]uses , androots[2i+1]multiplies by the new primitive -th root . Correctness. The FFT proof uses only , and (a geometric series with ratio whose -th power is 1), all of which hold for a primitive -th root of unity in any field; the arithmetic is exact, so no rounding is involved. The inverse is the forward transform followed by the index reversal and division by (invertible since ).Three primes. Each convolution value satisfies , so is determined by its residues. Garner’s mixed radix: , , , and with each digit in range, so reducing that expression modulo (with 128-bit intermediates) gives (Congruences and CRT). Checked against the quadratic product for random lengths, against all three NTT primes, and with and .
| Optimisation | |
|---|---|
| roots precomputed once per prime (static table above) | the per-level pow of naive code is a large constant |
Montgomery / Barrett butterflies, unsigned arithmetic without % | 2–3× faster NTT (Fast Modular Multiplication) |
| one inverse transform for several products (linearity) | e.g. in the frequency domain |
| squaring: one forward transform | |
| middle product / transposed multiplication | half-length transforms in Newton iterations (Power Series) |
Uses beyond contests: lattice-based cryptography and homomorphic encryption do all their polynomial ring arithmetic with NTTs; exact big-integer libraries use multi-prime NTT.
Related notes
- FFT (complex FFT, splitting, Karatsuba)
- Power Series (inverse, log, exp, sqrt on top of
polyMul) - Congruences and CRT (Garner)