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 FFT | NTT (NTT) | |
|---|---|---|
| arithmetic | double | integers mod |
| exactness | approximate, round at the end | exact |
| limits | about 53 bits of precision in the result | length ; needs a modulus (or CRT) |
| speed | comparable (complex mult = 4 real) | slightly slower per butterfly with % |
| use | real data, exact integers with small products, arbitrary modulus by splitting | everything modulo 998244353 |
| FFT variant | Idea |
|---|---|
| radix-2 DIT (below) | split by index parity; bit-reversal on input |
| radix-2 DIF | split by output parity; bit-reversal on output |
| radix-4 / split-radix | fewer multiplications, about 20% faster |
| Good–Thomas | coprime: no twiddle factors (Theoretical Algorithms) |
| Rader | prime 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;
}Proof: Cooley–Tukey, the inverse, the convolution theorem and the packing tricks
Butterfly. Split by parity: , where are DFTs of length because is a primitive -th root of unity; since , . Hence . The recursion reads the input in bit-reversed order, so permuting first and running the levels bottom-up (
rt[k + j], a primitive -th root to the power ) is the same computation. Inverse: (geometric series of a root of unity ), so applying the DFT with and dividing by inverts it; the DFT with is the DFT with followed by the index reversal , which is what the code does. Convolution theorem: , a cyclic convolution; padding to makes it the ordinary one (no index wraps).Packing two real inputs. For real , . With , and , so , and with , . Splitting (
convMod): write , ; then . Each of the four convolutions has terms below , so for the exact values stay below , inside double precision with long-double roots. Linearity of the inverse transform lets one complex inverse carry two real results (real and imaginary parts), giving 2 forward + 2 inverse FFTs. Precision: the rounding error grows roughly like ; with and coefficients a plain FFT loses the low bits, which is whyconvExacthas a bound andconvModsplits. All three were checked against the product (random lengths up to , modulus ).
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 .
| Problem | Combination |
|---|---|
| polynomial multiplication; ways to get sum from two sets; subsequence-pattern counts | |
| pairs at distance | (reverse one array) |
| string matching with wildcards | correlation (Other Matching) |
| big integers | with carries |
| XOR / AND / OR pairs | , , (Bitwise Convolutions) |
| gcd / product pairs | , : Dirichlet (Arithmetic Functions) |
| Combining rule | Transform | Cost |
|---|---|---|
| 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 max | base- DFT / zeta (Convolution Variants) | |
| / , both convex | merge slopes (Minkowski) | |
| , one convex | SMAWK / monotone minima | |
| , general | no 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).
| Optimisation | Effect |
|---|---|
| precompute the roots once | removes 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 FFT | halves 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.
| Method | Time | Typical crossover (limbs) |
|---|---|---|
| schoolbook | ||
| Karatsuba | 32 – 500 | |
| Toom-3 / Toom-4 | / (Toom-3) | 500 – 10000 |
| FFT / NTT | ||
| Schönhage–Strassen | very 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 operation | Method | Cost |
|---|---|---|
| add, subtract | schoolbook | |
| multiply | as above | |
| divide, modulo | Newton iteration on the reciprocal (correct bits double) | |
| square root | Newton | |
| base conversion | divide and conquer on powers of the base | |
| gcd | binary gcd / half-gcd | / |
| modular exponentiation | square 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.
Proof: Karatsuba and carry propagation
With , : and , so three half-size products suffice; by the master theorem. For polynomials is the variable and the identity is the same. Decimal product: the convolution of limb sequences gives the coefficients of the product in base 100 before carrying; propagating carries from the lowest limb keeps the value unchanged and makes every limb lie in , the unique base-100 representation. Checked against
__int128products and the quadratic method.
Related notes
- NTT (exact modular convolution, three-prime CRT)
- Power Series (everything built on multiplication)
- Theoretical Algorithms (Toom-3, Good–Thomas, Rader)