Purpose: Multiply two polynomials (or compute a discrete Fourier transform) in O(N log N) time, instead of O(N²) with naive convolution.
Algorithm
- Given a polynomial of degree
< N(padded to a power of 2), evaluate it at theN-th roots of unity using a divide-and-conquer approach: split the polynomial into its even-indexed and odd-indexed coefficients. - Recursively compute the FFT of each half (evaluating each half-sized polynomial at the squares of the roots of unity, which are exactly the roots of unity for the half-sized problem).
- Combine the two half-results using the “butterfly” operation: for each root of unity
ω, combine the even and odd parts aseven(ω²) + ω · odd(ω²)andeven(ω²) - ω · odd(ω²)to get values atωand-ωrespectively. - This gives the polynomial’s values at all
Nroots of unity (point-value representation). - To multiply two polynomials: FFT both, multiply the resulting point-values together element-wise, then apply the inverse FFT (same algorithm with conjugated roots of unity, scaled by
1/N) to convert back to coefficient representation.
Code
typedef complex<double> cd;
const double PI = acos(-1);
void fft(vector<cd>& a, bool invert) {
int n = a.size();
if (n == 1) return;
vector<cd> a0(n / 2), a1(n / 2);
for (int i = 0; i < n / 2; i++) {
a0[i] = a[2 * i];
a1[i] = a[2 * i + 1];
}
fft(a0, invert);
fft(a1, invert);
double ang = 2 * PI / n * (invert ? -1 : 1);
cd w(1), wn(cos(ang), sin(ang));
for (int i = 0; i < n / 2; i++) {
cd t = w * a1[i];
a[i] = a0[i] + t;
a[i + n / 2] = a0[i] - t;
if (invert) { a[i] /= 2; a[i + n / 2] /= 2; }
w *= wn;
}
}
vector<long long> multiply(vector<long long>& a, vector<long long>& b) {
vector<cd> fa(a.begin(), a.end()), fb(b.begin(), b.end());
int n = 1;
while (n < (int)(a.size() + b.size())) n <<= 1;
fa.resize(n); fb.resize(n);
fft(fa, false); fft(fb, false);
for (int i = 0; i < n; i++) fa[i] *= fb[i];
fft(fa, true);
vector<long long> result(n);
for (int i = 0; i < n; i++) result[i] = round(fa[i].real());
return result;
}Paradigm
Divide and Conquer. The polynomial is split into even/odd halves, each recursively transformed, and combined via the butterfly operation — a direct halving recursion analogous to merge sort’s structure.
Complexity
- Time: O(N log N)
- Space: O(N log N) for the recursion (O(N) with an iterative in-place version)
Proof of Correctness
Why splitting into even/odd coefficients works: For a polynomial A(x) = A_even(x²) + x·A_odd(x²) (splitting coefficients by parity), evaluating at the N-th roots of unity ω^0, ω^1, ..., ω^(N-1) requires evaluating A_even and A_odd at ω^(2k) for various k — but since ω is a primitive N-th root of unity, ω² is a primitive (N/2)-th root of unity, meaning A_even and A_odd only ever need to be evaluated at the (N/2)-th roots of unity, exactly matching a half-sized FFT problem recursively.
Why the butterfly combination correctly recovers both A(ω^k) and A(ω^(k+N/2)): Since ω^(k+N/2) = -ω^k (a defining property of roots of unity — the two halves are always negatives of each other), and A(x) = A_even(x²) + x·A_odd(x²):
A(ω^k) = A_even((ω^k)²) + ω^k · A_odd((ω^k)²)
A(-ω^k) = A_even((ω^k)²) - ω^k · A_odd((ω^k)²)
Since (ω^k)² = (-ω^k)² (squaring removes the sign), both expressions reuse the exact same recursively-computed A_even and A_odd values at (ω^k)², differing only by the sign of the ω^k · A_odd(...) term — exactly the butterfly’s add/subtract combination.
Why convolution via pointwise multiplication works: Evaluating two polynomials at the same N points, multiplying pointwise, and interpolating back recovers the coefficient vector of their product, since polynomial multiplication in coefficient form corresponds exactly to pointwise multiplication in point-value form (a degree-< N polynomial is uniquely determined by its values at N distinct points — a standard fact from polynomial interpolation theory), as long as N is chosen large enough to hold the product’s degree without aliasing/wraparound.
Why the inverse FFT recovers coefficients correctly: Using conjugate (or negated-angle) roots of unity and scaling by 1/N inverts the transform, which follows directly from the orthogonality of roots of unity (Σ ω^(jk) = 0 unless j ≡ 0 mod N, in which case it equals N) — a standard discrete Fourier transform duality property. ∎
Variants / Use Cases
- Number Theoretic Transform (NTT) → FFT analogue over a finite field, avoiding floating-point precision issues for integer/modular convolution
- Fast Walsh-Hadamard Transform (FWHT) → analogous divide-and-conquer transform for XOR/AND/OR convolutions instead of standard polynomial convolution
- Big integer multiplication → treating digits as polynomial coefficients, FFT-based multiplication achieves near-linear time for huge numbers
- Signal processing (audio, image compression) → FFT is foundational to spectral analysis, filtering, and compression (e.g., JPEG’s DCT is closely related)
- String matching with wildcards → FFT-based convolution can check pattern matches allowing wildcard characters in O(N log N)