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.

PrimeFormMax lengthPrimitive root
9982443533
1677721613
4697620493
10045358093
75497472111
201326592131
41793404541998202893 (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;
}
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 multiplicationhalf-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.