Evaluate at the -th roots of unity, multiply pointwise, interpolate back: convolution becomes . This note has the complex FFT (real coefficients, exact integer products by rounding, arbitrary modulus by splitting), the convolution toolkit, Karatsuba and big-number multiplication. Exact modular convolution is the NTT, which most later notes use.

evaluate   A and B at the n-th roots of unity    O(n log n)
multiply   pointwise                             O(n)
interpolate (inverse transform)                  O(n log n)
complex FFTNTT (NTT)
arithmeticdoubleintegers mod
exactnessapproximate, round at the endexact
limitsabout 53 bits of precision in the resultlength ; needs a modulus (or CRT)
speedcomparable (complex mult = 4 real)slightly slower per butterfly with %
usereal data, exact integers with small products, arbitrary modulus by splittingeverything modulo 998244353
FFT variantIdea
radix-2 DIT (below)split by index parity; bit-reversal on input
radix-2 DIFsplit by output parity; bit-reversal on output
radix-4 / split-radixfewer multiplications, about 20% faster
Good–Thomas coprime: no twiddle factors (Theoretical Algorithms)
Raderprime via a cyclic convolution over the multiplicative group
Bluestein (chirp-z)any , and evaluation at a geometric progression (Convolution Variants)
using cd = complex<double>;
void fft(vector<cd>& a, bool invert = false) {              // n a power of two; roots built in long double
    int n = sz(a); if (n <= 1) return;
    static vector<complex<long double>> R(2, 1); static vector<cd> rt(2, 1);   // rt[k + j] = e^{i pi j / k}
    for (static int k = 2; k < n; k *= 2) {
        R.resize(n); rt.resize(n); auto x = polar(1.0L, acosl(-1) / k);
        for (int i = k; i < 2 * k; i++) rt[i] = R[i] = i & 1 ? R[i / 2] * x : R[i / 2];
    }
    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++) {
            cd z = rt[j + k] * a[i + j + k];
            a[i + j + k] = a[i + j] - z; a[i + j] += z;
        }
    if (invert) { reverse(a.begin() + 1, a.end()); for (auto& x : a) x /= n; }
}
 
vector<double> convReal(const vector<double>& a, const vector<double>& b) {   // one forward + one inverse FFT (packing)
    if (a.empty() || b.empty()) return {};
    int need = sz(a) + sz(b) - 1, n = 1; while (n < need) n *= 2;
    vector<cd> f(n); for (int i = 0; i < sz(a); i++) f[i].real(a[i]); for (int i = 0; i < sz(b); i++) f[i].imag(b[i]);
    fft(f);
    vector<cd> p(n);
    for (int k = 0; k < n; k++) {                           // A_k = (F_k + conj F_{-k}) / 2, B_k = (F_k - conj F_{-k}) / 2i
        cd x = f[k], y = conj(f[(n - k) & (n - 1)]);
        p[k] = (x + y) * (x - y) / cd(0, 4);
    }
    fft(p, true);
    vector<double> res(need); for (int i = 0; i < need; i++) res[i] = p[i].real();
    return res;
}
 
vector<ll> convExact(const vector<ll>& a, const vector<ll>& b) {   // exact while every result (and sum |a||b|) stays below ~1e15
    vector<double> x(all(a)), y(all(b)); auto r = convReal(x, y);
    vector<ll> res(sz(r)); for (int i = 0; i < sz(r); i++) res[i] = llround(r[i]);
    return res;
}
 
vector<ll> convMod(const vector<ll>& a, const vector<ll>& b, ll M) {   // any M < 2^30, entries in [0, M); 4 FFTs
    if (a.empty() || b.empty()) return {};
    int need = sz(a) + sz(b) - 1, n = 1; while (n < need) n *= 2;
    const int cut = 1 << 15;
    vector<cd> P(n), Q(n);
    for (int i = 0; i < sz(a); i++) P[i] = cd(a[i] / cut, a[i] % cut);   // hi + i lo
    for (int i = 0; i < sz(b); i++) Q[i] = cd(b[i] / cut, b[i] % cut);
    fft(P); fft(Q);
    vector<cd> X(n), Y(n);
    for (int k = 0; k < n; k++) {
        int r = (n - k) & (n - 1);
        cd a1 = (P[k] + conj(P[r])) / 2.0, a0 = (P[k] - conj(P[r])) / cd(0, 2);
        cd b1 = (Q[k] + conj(Q[r])) / 2.0, b0 = (Q[k] - conj(Q[r])) / cd(0, 2);
        X[k] = a1 * b1 + cd(0, 1) * (a1 * b0); Y[k] = a0 * b1 + cd(0, 1) * (a0 * b0);
    }
    fft(X, true); fft(Y, true);
    vector<ll> res(need);
    for (int i = 0; i < need; i++) {
        ll hh = llround(X[i].real()) % M, hl = llround(X[i].imag()) % M, lh = llround(Y[i].real()) % M, ll_ = llround(Y[i].imag()) % M;
        res[i] = ((hh * cut % M * cut + (hl + lh) % M * cut + ll_) % M + M) % M;
    }
    return res;
}

Convolution: recognising and choosing

is the single most important operation of this chapter: recognising one turns a double loop into . The answer at index sums over all pairs of indices combining to .

ProblemCombination
polynomial multiplication; ways to get sum from two sets; subsequence-pattern counts
pairs at distance (reverse one array)
string matching with wildcardscorrelation (Other Matching)
big integers with carries
XOR / AND / OR pairs, , (Bitwise Convolutions)
gcd / product pairs, : Dirichlet (Arithmetic Functions)
Combining ruleTransformCost
FFT / NTT
(cyclic)DFT of length exactly (Bluestein) or linear product then fold
Walsh–Hadamard
, subset / superset zeta and Möbius
with subset convolution (ranked zeta)
Dirichlet convolution
prefix sums
digitwise sum mod , digitwise maxbase- DFT / zeta (Convolution Variants)
/ , both convexmerge slopes (Minkowski)
, one convexSMAWK / monotone minima
, generalno truly subquadratic algorithm known

All transform-based ones share the shape transform(a); transform(b); c[i] = a[i] * b[i]; inverse(c), only the transform differs. Without a transform: sparse inputs with non-zeros cost directly; below –64 the quadratic loop wins; convex max-plus merges difference sequences (Minkowski Sum); prefix-shaped kernels are prefix sums. Multi-dimensional: transform along each axis, or flatten with a stride so rows do not wrap. Precision and modulus: small integer coefficients NTT; arbitrary prime modulus three NTT primes + CRT, or convMod; real coefficients complex FFT; huge integers without modulus split into 15-bit halves. The general lesson: when a problem says “count the ways to combine one thing from each of two sets”, write down the generating function; the answer is a coefficient of a product (Generating Functions).

OptimisationEffect
precompute the roots onceremoves a large constant (the code above caches them)
lazy reduction (Barrett / Montgomery, Fast Modular Multiplication)fewer % in NTT
skip the inverse transform when one coefficient is needed directly
reuse transforms; squaring needs one forward transform
pack two real sequences into one complex FFThalves the forward work (convReal)

Karatsuba and big multiplication

Multiplying -digit numbers is polynomial multiplication plus carry propagation. All of the ladder below is evaluate, multiply pointwise, interpolate: Karatsuba evaluates at ; Toom-3 at ; Toom- at small points; FFT at the roots of unity, where both evaluation and interpolation cost instead of with a large constant, which is why FFT eventually wins. Complex multiplication in 3 real multiplications is the smallest instance of the trick; Strassen applies it to blocks.

MethodTimeTypical crossover (limbs)
schoolbook
Karatsuba32 – 500
Toom-3 / Toom-4 / (Toom-3)500 – 10000
FFT / NTT
Schönhage–Strassenvery large
Fürer; Harvey–van der Hoeven; galactic
vector<ll> karatsuba(vector<ll> a, vector<ll> b) {          // exact integer polynomial product
    if (a.empty() || b.empty()) return {};
    int n = max(sz(a), sz(b));
    if (min(sz(a), sz(b)) <= 32) { vector<ll> r(sz(a) + sz(b) - 1, 0); for (int i = 0; i < sz(a); i++) for (int j = 0; j < sz(b); j++) r[i + j] += a[i] * b[j]; return r; }
    int m = (n + 1) / 2, need = sz(a) + sz(b) - 1; a.resize(2 * m, 0); b.resize(2 * m, 0);
    vector<ll> a0(a.begin(), a.begin() + m), a1(a.begin() + m, a.end()), b0(b.begin(), b.begin() + m), b1(b.begin() + m, b.end());
    auto z0 = karatsuba(a0, b0), z2 = karatsuba(a1, b1);
    for (int i = 0; i < m; i++) a0[i] += a1[i], b0[i] += b1[i];
    auto z1 = karatsuba(a0, b0);
    for (int i = 0; i < sz(z0); i++) z1[i] -= z0[i];
    for (int i = 0; i < sz(z2); i++) z1[i] -= z2[i];
    vector<ll> r(4 * m, 0);
    for (int i = 0; i < sz(z0); i++) r[i] += z0[i];
    for (int i = 0; i < sz(z1); i++) r[i + m] += z1[i];
    for (int i = 0; i < sz(z2); i++) r[i + 2 * m] += z2[i];
    r.resize(need); return r;
}
 
string multiplyDecimal(const string& s, const string& t) {   // non-negative decimal strings, base-100 limbs + FFT
    auto limbs = [](const string& x) { vector<ll> v; for (int i = sz(x); i > 0; i -= 2) v.push_back(stoi(x.substr(max(0, i - 2), i - max(0, i - 2)))); return v; };
    vector<ll> c = convExact(limbs(s), limbs(t));
    ll carry = 0;
    for (auto& x : c) { x += carry; carry = x / 100; x %= 100; }
    while (carry) c.push_back(carry % 100), carry /= 100;
    while (sz(c) > 1 && c.back() == 0) c.pop_back();
    string r = to_string(c.back());
    for (int i = sz(c) - 2; i >= 0; i--) { string d = to_string(c[i]); r += string(2 - sz(d), '0') + d; }
    return r;
}

Base choice. With base and limbs the convolution values reach ; doubles hold about 53 bits, so : base 100 is safe far beyond digits, base is marginal at ; NTT (or three primes) has no such limit. Karatsuba only wins above a threshold (32–64 limbs), below which recursion overhead loses to a tight schoolbook loop; every serious bignum library (GMP, Java BigInteger, Python int) switches between schoolbook, Karatsuba, Toom and FFT at tuned cutoffs.

Big-integer operationMethodCost
add, subtractschoolbook
multiplyas above
divide, moduloNewton iteration on the reciprocal (correct bits double)
square rootNewton
base conversiondivide and conquer on powers of the base
gcdbinary gcd / half-gcd /
modular exponentiationsquare and multiply with Montgomery

In contests you rarely need big integers: answers modulo a prime, __int128 up to for comparisons like vs , fast exponentiation, Legendre’s formula for factorial valuations; when exact huge outputs are required (exact Fibonacci or factorials, -digit answers), a base- schoolbook implementation (Big Integers and Fractions) suffices up to a few thousand digits, and FFT multiplication beyond.