Cairn
algorithms · number theory · O(n log n) via the n-th roots of unity

back to Number Theory

Fast Fourier Transform

The site's twelfth Number Theory entry, and the other half of a forward reference Karatsuba Multiplication left open in its own closing paragraph: "the fastest practical algorithms multiply via Fast Fourier Transform-based convolution." Karatsuba speeds up multiplying two numbers by splitting each into halves and reusing the sum of the halves — still O(n1.585). The Fast Fourier Transform (FFT) attacks a more general version of the same underlying problem — multiplying (convolving) two coefficient sequences directly — in O(n log n), and is exactly the technique real bignum libraries reach for once a number outgrows the range where even Karatsuba's split-in-half trick is the fastest option. Multiplying two n-coefficient polynomials the schoolbook way (every coefficient of one against every coefficient of the other) costs O(n²) multiplications, the same quadratic wall Karatsuba climbs over for plain integers. The FFT climbs over it for convolution itself: evaluate both polynomials at n cleverly chosen points, multiply the values pointwise in O(n), then interpolate back to coefficients — and the specific choice of points (the complex n-th roots of unity) is what makes both the evaluation and the interpolation themselves computable in O(n log n) instead of the O(n²) a naive choice of points would cost.

Try it

Enter two small polynomials as comma-separated coefficients, constant term first (so 1,2,3 means 1 + 2x + 3x²), up to 4 coefficients each, then press Build. Step through all four phases: a forward FFT of A, a forward FFT of B (each its own recursion tree — split by index parity, recurse, then "butterfly"-combine the two halves), a pointwise multiplication of the two point-value arrays, and an inverse FFT of the product that recovers the final coefficients. The two checkboxes reproduce real, checked bugs in the inverse step — see Pitfalls.

Press Build.

Why it works

A polynomial of degree less than n is fully determined either way: by its n coefficients, or by its value at any n distinct points. That gives a second route to multiplying two polynomials A and B: evaluate both at the same n points, multiply the values pointwise, then interpolate the product back to coefficients — the pointwise step is O(n), but evaluating or interpolating a degree-n polynomial at n arbitrary points still costs O(n²) in general, so nothing has been won yet.

The FFT wins by choosing the n points deliberately: the complex n-th roots of unity, ωnk = e2πik/n for k = 0..n-1. Split a polynomial's coefficients by index parity, P(x) = Peven(x²) + x·Podd(x²), where Peven and Podd are half-size polynomials built from the even- and odd-indexed coefficients. Squaring an n-th root of unity gives an (n/2)-th root of unity, and every (n/2)-th root arises from exactly two of the n starting points (ωnk and ωnk+n/2 square to the same value, since ωnk+n/2 = -ωnk) — so evaluating P at all n roots reduces to evaluating Peven and Podd at only n/2 points each, then combining each pair with one multiplication (the "butterfly"): P(ωk) = Peven2k) + ωk·Podd2k) and P(ωk+n/2) = Peven2k) - ωk·Podd2k). That's the recurrence T(n) = 2T(n/2) + O(n), which solves to O(n log n) — the exact recursion tree the demo above steps through, twice (once for A, once for B). Interpolating back is the identical structure run in reverse: the inverse transform uses the same roots' complex conjugates (ω-1 in place of ω) and divides the final result by n once at the end, recovering the coefficients from the pointwise product of the two forward transforms — the convolution theorem, that pointwise-multiplying two sequences' transforms is the transform of their convolution.

Reference implementation

This is the exact algorithm the demo above steps through (numbers padded with zeros to the next power of two n, since the recursive even/odd split needs a power-of-two length at every level):

function nextPow2(n) { let p = 1; while (p < n) p *= 2; return p; }
function cadd(a, b) { return { re: a.re + b.re, im: a.im + b.im }; }
function csub(a, b) { return { re: a.re - b.re, im: a.im - b.im }; }
function cmul(a, b) { return { re: a.re*b.re - a.im*b.im, im: a.re*b.im + a.im*b.re }; }

function fft(a, invert) {                        // a: array of {re, im}, length a power of two
  const n = a.length;
  if (n === 1) return [a[0]];
  const evens = fft(a.filter((_, i) => i % 2 === 0), invert);
  const odds  = fft(a.filter((_, i) => i % 2 === 1), invert);
  const result = new Array(n);
  for (let k = 0; k < n / 2; k++) {
    const angle = (invert ? 1 : -1) * 2 * Math.PI * k / n;      // conjugate direction -- see Pitfalls
    const w = { re: Math.cos(angle), im: Math.sin(angle) };
    const t = cmul(w, odds[k]);
    result[k] = cadd(evens[k], t);
    result[k + n / 2] = csub(evens[k], t);
  }
  return result;
}

function multiply(aCoeffs, bCoeffs) {
  const resultLen = aCoeffs.length + bCoeffs.length - 1;
  const n = nextPow2(resultLen);
  const A = aCoeffs.map(v => ({ re: v, im: 0 })); while (A.length < n) A.push({ re: 0, im: 0 });
  const B = bCoeffs.map(v => ({ re: v, im: 0 })); while (B.length < n) B.push({ re: 0, im: 0 });
  const FA = fft(A, false), FB = fft(B, false);
  const FC = FA.map((v, i) => cmul(v, FB[i]));
  const inv = fft(FC, true);
  return inv.slice(0, resultLen).map(v => Math.round(v.re / n));   // divide by n -- see Pitfalls
}

Verified against direct convolution exhaustively (every pair of polynomials with 1–3 coefficients each, values -2 to 2, 24,025 combinations, 0 mismatches) and against 8,000 randomized trials spanning every combination of polynomial lengths from 1 to 4 on each side with coefficients -9 to 9, 0 mismatches in both sweeps.

Pitfalls

Skipping the division by n after the inverse transform is a checked bug, and the damage is exact. The first checkbox above reproduces it: every returned coefficient comes out exactly too large, where n is the padded transform length — not an approximation, confirmed across several different polynomial shapes and sizes (each one's output matched the true convolution multiplied by that run's own n, with zero exceptions). On this page's own default, (1+2x+3x²)(4+5x+6x²) padded to n=8, the correct product is [4, 13, 28, 27, 18] (4 + 13x + 28x² + 27x³ + 18x⁴); with the division skipped it reports [32, 104, 224, 216, 144] — every entry exactly 8× too large.

Reusing the forward transform's twiddle direction in the inverse step, instead of conjugating it, is a checked bug too — and it doesn't just scale the answer, it scrambles it. The second checkbox computes the "inverse" using the same angle = -2πk/n the forward pass uses, rather than flipping its sign. Applying the forward transform twice in a row to any sequence is a known identity: it returns n times that sequence circularly reversed (index 0 stays put, index k swaps with index n-k) — and dividing by n still happens in this buggy path, so what comes out is exactly that reversal, not a scaled reversal. Checked directly on this page's own default: the correct (zero-padded) product is [4, 13, 28, 27, 18, 0, 0, 0]; the buggy path returns [4, 0, 0, 0, 18, 27, 28, 13] — position 0 unchanged, position 4 unchanged (its own mirror), and positions 1–3 swapped with 7–5 exactly as the reversal identity predicts. Across 2,000 randomized trials this disagreed with the correct product 99.9–100% of the time (varies slightly by random seed) — the rare exceptions are cases where the true zero-padded convolution already happens to be its own circular reversal, mostly when negative and positive random coefficients cancel out the higher terms to zero by chance, not a weakness in the check.

Complexity

O(n log n) complex multiplications, where n is the padded length (the next power of two at least as large as the combined coefficient count minus one), against schoolbook convolution's O(n²). That asymptotic win doesn't show up immediately: on this page's own default (3 coefficients each side, padded to n=8), the reference implementation above performs 44 complex multiplications (measured directly from the algorithm, not estimated) against schoolbook's 3×3 = 9 real multiplications — the FFT is worse here, the same small-size "modest saving" story Karatsuba's own page tells at its own default. Measured across growing equal-length inputs, the crossover is real and lands between 16 and 32 coefficients per side:

coefficients per sideFFT complex multiplicationsschoolbook real multiplications
44416
811264
16272256
326401,024
641,4724,096
1283,32816,384
2567,42465,536

Worth being precise about what's being compared: a complex multiplication itself costs more than a single real one — four real multiplications done naively, or three using the identical split-the-work trick Karatsuba's own page uses to cut a multiplication count from four to three, since (a+bi)(c+di) = (ac-bd) + (ad+bc)i needs only ac, bd, and (a+b)(c+d) to recover all three real quantities. So the real wall-clock crossover sits somewhat to the right of the raw table above, not exactly at 16–32 — but the asymptotic story is unaffected, and the gap only widens: by 256 coefficients per side, schoolbook needs almost 9× as many multiplications, not accounting for that per-multiplication constant at all.

Space: O(n) for the padded coefficient and point-value arrays, with O(log n) recursion depth, the same shape as Karatsuba's own bound. This is the technique real bignum libraries (GMP, Python's arbitrary-precision int) reach for once a number outgrows the range where Karatsuba's O(n1.585) is still the fastest option — treating the number's digits as polynomial coefficients, multiplying via this exact evaluate/multiply/interpolate strategy, then resolving carries afterward. Production implementations use fixed-point number-theoretic transforms over a finite field instead of floating-point complex numbers, avoiding the rounding this page's own Math.round depends on, but the recursive structure is identical to what the demo above steps through.

This site's guide, Choosing a Number Theory Algorithm, sets this page and its two siblings aside from the rest of the category — every other Number Theory entry treats a multiplication as a single cheap step, an assumption that stops holding once the numbers run to hundreds of digits. This is the top rung of that four-tier ladder: the one worth its own larger constant factor only once Karatsuba and Toom-Cook have both been outgrown.