The Fast Fourier Transform
Sixteen multiplications for one product
Multiply two polynomials:
Store each as its list of coefficients, lowest power first: = [5, 2, 7, 3] and =
[4, 0, 6, 1]. The product has degree 6, and its coefficient of collects every pair of
terms whose powers add up to :
This list is called the convolution of and , written . The schoolbook method computes it by multiplying every by every .
import cmath, math, random
def schoolbook(a, b, count):
"""The convolution c = a * b; count[0] gains one per coefficient product."""
c = [0] * (len(a) + len(b) - 1)
for i, x in enumerate(a):
for j, y in enumerate(b):
count[0] += 1
c[i + j] += x * y
return c
A, B = [5, 2, 7, 3], [4, 0, 6, 1]
count = [0]
C = schoolbook(A, B, count)
assert C == [20, 8, 58, 29, 44, 25, 3] and count[0] == 16
assert C[3] == 5 * 1 + 2 * 6 + 7 * 0 + 3 * 4 == 29
Two lists of coefficients take multiplications. That is 16 here and at . The same sum appears whenever numbers are multiplied digit by digit: the integer 3725 is with its digits as coefficients, so multiplying two 100,000-digit integers is a convolution of their digit lists followed by carrying. This module computes a convolution of length in arithmetic operations.
Values multiply; coefficients don't
A polynomial can also be described by its values. Evaluate and at : and , so , one multiplication. Values multiply pointwise.
And values determine a polynomial. If two polynomials of degree less than agree at distinct points, their difference has degree less than but roots, so it is the zero polynomial. (That uses the fact that a nonzero polynomial of degree has at most roots, which holds over the complex numbers and over the integers modulo a prime; module 24 proves it and builds much more on it.) has degree 6, so 7 of its values fix it. That suggests a plan:
- evaluate and at points,
- multiply the values pointwise, multiplications,
- interpolate: recover the 7 coefficients of from its values.
For two inputs of length , the product has coefficients, so .
def horner(a, x):
v = 0
for coef in reversed(a):
v = v * x + coef
return v
assert horner(A, 2) == 61 and horner(B, 2) == 36
assert horner(C, 2) == 61 * 36 == 2196
The plan moves the waste instead of removing it. Horner's rule costs multiplications per point, so evaluating at arbitrary points costs about , and interpolating back from arbitrary points costs by Lagrange's formula. Each point is evaluated from scratch: nothing computed for helps with . The rest of this module chooses the points so that their evaluations share almost all of their work.
Predict: which 8 points would you choose, and why might 1 and −1 be a good start?
Split into its even and odd parts: . At and the two brackets take the same values, 12 and 5, because for both. So and share their work. We want eight points that pair up like that, whose squares pair up again, and so on down. The eighth roots of unity do.
The model
We count arithmetic operations on exact values of a number system in which the points we need exist: the complex numbers, treated as exact, or the integers modulo a prime . The operation we count is multiplication. Every multiplication in the algorithm comes with one addition and one subtraction, so additions change the total by a constant factor. The algorithm flips no coins, and every bound below is worst case over all inputs, for a length that is a power of two (shorter inputs are padded with zeros, which at most doubles ). What happens when complex numbers are rounded to floating point is a separate question, answered in its own section.
Roots of unity and the even/odd split
A number is a primitive -th root of unity if and for . Over the complex numbers, , and its powers are equally spaced points on the unit circle. The discrete Fourier transform (DFT) of a coefficient list is the list of values
Two facts about these points do all the work, for even:
- Halving lemma. is a primitive -th root of unity. So squaring the points gives the points of the half-size problem, each twice.
- Opposite points. , so .
The second fact is where primitivity matters: gives , and is ruled out because .
Now split by the parity of its powers. Let hold the even-indexed coefficients and the odd-indexed ones, each a polynomial with coefficients, so that
At and at the square is the same point . So for :
The values for are exactly the DFT of at the -th roots, and likewise for . One product , one addition and one subtraction give two outputs. That pair of lines is called a butterfly.
Invariant
A DFT of length is two DFTs of length , of the even and the odd coefficients, joined by butterflies: output and output are at the same point .
Recursing on and gives the fast Fourier transform (FFT), in the form published by Cooley and Tukey in 1965. Here it is as a recursion, with the root chosen by a helper so that the same code runs over the complex numbers or modulo a prime .
def root(N, k, invert, p):
"""omega^k (or omega^-k) for a primitive N-th root omega: complex, or mod p."""
if p is None:
return cmath.exp((-2j if invert else 2j) * math.pi * k / N)
w = pow(3, (p - 1) // N, p) # why 3 works: see "Exact or approximate"
return pow(w, (N - k) % N if invert else k, p)
def fft(a, invert=False, p=None, count=None):
"""The DFT of a (len(a) a power of two); count[0] gains one per butterfly product."""
N = len(a)
if N == 1:
return list(a)
E = fft(a[0::2], invert, p, count)
O = fft(a[1::2], invert, p, count)
out = [0] * N
for k in range(N // 2):
t = root(N, k, invert, p) * O[k]
if count is not None:
count[0] += 1
out[k], out[k + N // 2] = E[k] + t, E[k] - t
if p is not None:
out[k], out[k + N // 2] = out[k] % p, out[k + N // 2] % p
return out
Tracing with eight points
Pad to length 8 (the product needs ) and split it all the way down. Each level separates the even and odd positions of every piece.
The leaf order is not an accident. Write the index in three bits. The first split sorts by the
last bit, the second by the middle bit, the third by the first bit, so the leaf in position
holds the coefficient whose index is with its bits reversed: position 1 is 001, which
holds index 100 = 4. An iterative FFT therefore permutes its input into this bit-reversed order
once, then combines neighbouring blocks of length 2, 4, 8, … in place.
Predict: for N = 8, in which leaf position does coefficient 6 end up? And coefficient 3?
6 is 110, reversed 011 = 3, so coefficient 6 sits in position 3 (the leaves above are
0, 4, 2, 6, …). 3 is 011, reversed 110 = 6. Bit reversal swaps them.
def leaf_order(idx):
"""The leaves of the even/odd recursion on a list of indices, left to right."""
if len(idx) == 1:
return idx
return leaf_order(idx[0::2]) + leaf_order(idx[1::2])
assert (A + [0] * 4)[0::2] == [5, 7, 0, 0] and (A + [0] * 4)[1::2] == [2, 3, 0, 0]
assert leaf_order(list(range(8))) == [0, 4, 2, 6, 1, 5, 3, 7]
assert all(int(format(j, "03b")[::-1], 2) == leaf for j, leaf in enumerate(leaf_order(list(range(8)))))
Now the top butterflies. and (the zeros drop out), evaluated at the fourth roots , which are for and . Each row is one butterfly, giving outputs and .
| 0 | |||||
| 1 | |||||
| 2 | |||||
| 3 |
Row 2 is worth doing by hand: , so , and indeed . Row 0 is the pair from the prediction above.
E4 = [5 + 7 * y for y in (1, 1j, -1, -1j)]
O4 = [2 + 3 * y for y in (1, 1j, -1, -1j)]
assert E4 == [12, 5 + 7j, -2, 5 - 7j] and O4 == [5, 2 + 3j, -1, 2 - 3j]
w8 = cmath.exp(2j * math.pi / 8)
vals = fft(A + [0] * 4)
for k in range(8):
assert abs(vals[k] - horner(A, w8 ** k)) < 1e-9 # the DFT is evaluation
assert abs(vals[k] - (E4[k % 4] + w8 ** k * O4[k % 4])) < 1e-9 # ... joined by butterflies
assert abs(vals[2] - (-2 - 1j)) < 1e-12 and abs(vals[0] - 17) < 1e-12 and abs(vals[4] - 7) < 1e-12
assert [round(v.real, 2) for v in vals[1:4:2]] == [4.29, 5.71]
assert [round(vals[1].imag, 2), round(vals[5].imag, 2)] == [10.54, 3.46]
Why it is correct, and how to go back
Correctness of the transform is induction on . For the DFT of is . For larger , the recursive calls return the DFTs of and at the -th roots (by the halving lemma, is the right root for them), and the two butterfly lines are the identity at . The step that must not be skipped is : with a root that is not primitive, output is not the negated partner of output , and the halves do not share their work.
The inverse is the same algorithm. Write the DFT as a matrix with entries . The product of with the matrix of has entries
If every term is 1 and the sum is . Otherwise the terms form a geometric series with ratio (primitivity again), and its sum is because . So the inverse of is times the matrix of : interpolate by running the FFT with and dividing by .
The product follows. Evaluation respects multiplication, so the pointwise product of the two DFTs is the DFT of the product polynomial, provided that polynomial has fewer than coefficients. Here is the whole algorithm, over the complex numbers (rounded at the end) or modulo :
def multiply(a, b, p=None, count=None):
m = len(a) + len(b) - 1
N = 1
while N < m:
N *= 2
fa = fft(a + [0] * (N - len(a)), False, p, count)
fb = fft(b + [0] * (N - len(b)), False, p, count)
if count is not None:
count[0] += 2 * N # N pointwise products, N scalings
prod = [x * y if p is None else x * y % p for x, y in zip(fa, fb)]
back = fft(prod, True, p, count)
if p is None:
return [round((v / N).real) for v in back[:m]]
n_inv = pow(N, p - 2, p) # 1/N mod p, by Fermat's little theorem
return [v * n_inv % p for v in back[:m]]
P = 998244353
assert multiply(A, B) == C and multiply(A, B, P) == C
Two mistakes produce plausible wrong answers. With too few points, for our pair, the
product's seven coefficients do not fit: at every fourth root of unity, so
folds onto , onto , and so on. The result is the cyclic convolution
, [64, 33, 61, 29]. Forgetting to divide by returns every coefficient
eight times too large.
fa, fb = fft(A, False, P), fft(B, False, P) # N = 4: too few points
wrapped = [v * pow(4, P - 2, P) % P for v in fft([x * y % P for x, y in zip(fa, fb)], True, P)]
assert wrapped == [64, 33, 61, 29] == [C[k] + (C[k + 4] if k + 4 < 7 else 0) for k in range(4)]
Complexity
Let count the multiplications of one transform. The two recursive calls cost each and the butterflies , so with . The recursion has levels, each doing butterflies, so exactly
Compare module 01: median of medians satisfies , whose subproblems add up to , so the level sums shrink geometrically and the total is linear. Here the subproblems add up to exactly , every level costs the same, and the factor stays.
A product makes two forward transforms, pointwise products, one inverse transform and scalings. With a power of two, that is
multiplications.
At the next power of two above is , and the count is 7,602,176, against for the schoolbook method. For small the schoolbook method wins: at it needs 256 multiplications and the transform 304. From on (1,024 against 704) the transform is cheaper. Space is .
def product_cost(n):
N = 1
while N < 2 * n - 1:
N *= 2
return 3 * (N // 2) * (N.bit_length() - 1) + 2 * N
for n in (16, 32, 64, 1024):
count = [0]
x = [random.Random(n).randrange(1000) for _ in range(n)]
assert multiply(x, x, P, count) == schoolbook(x, x, [0])
assert count[0] == product_cost(n)
assert (product_cost(16), product_cost(32)) == (304, 704)
assert product_cost(100_000) == 7_602_176
Is the end? For the DFT, is the best known algorithm. No lower bound above linear is known in general arithmetic models. Morgenstern (1973) proved for a restricted class: linear algorithms whose constants are bounded in absolute value. So the FFT is optimal among algorithms of that kind, and nobody knows whether something else could be faster.
Exact or approximate
Modulo a prime. The integers mod have a primitive -th root of unity exactly when divides . The prime is chosen for that: , and 3 generates all of the nonzero residues, so is a primitive -th root for every power of two . The transform modulo (often called the number-theoretic transform, NTT) makes no rounding error at all. It returns each , which is itself whenever . Past that it is silently wrong as an integer: comes back as 601,755,647. Larger coefficients need two or three primes combined by the Chinese remainder theorem, or smaller digits.
assert P - 1 == 119 * 2 ** 23 and 119 == 7 * 17
assert all(pow(3, (P - 1) // q, P) != 1 for q in (2, 7, 17)) # 3 generates mod P
w = pow(3, (P - 1) // 8, P)
assert pow(w, 8, P) == 1 and pow(w, 4, P) == P - 1 # primitive 8th root
assert multiply([40000], [40000], P) == [601_755_647] == [40000 ** 2 - P]
Big integers. Take . Their digit lists, least significant first, convolve to 15 sums of which the largest is 190, far below , so the transform mod gives them exactly. Carrying from the lowest digit up turns the sums into the product.
def big_product(x, y):
digits = lambda z: [int(ch) for ch in str(z)[::-1]]
sums = multiply(digits(x), digits(y), P)
out, carry = [], 0
for s in sums:
carry += s
out.append(carry % 10)
carry //= 10
while carry:
out.append(carry % 10)
carry //= 10
return int("".join(map(str, reversed(out)))), sums
value, sums = big_product(90417263, 58820419)
assert sums == [27, 57, 36, 89, 30, 101, 84, 190, 113, 118, 75, 55, 92, 72, 45]
assert value == 90417263 * 58820419 == 5318381294493197
Over the complex numbers in floating point. A double has a 53-bit significand, every butterfly rounds, and rounding the final values to integers is right only while the accumulated error stays below . That error grows with the size of the coefficients and with the number of levels. Having every exact coefficient below is not enough. Here are two random lists of 8,192 entries below : every coefficient of the product is below , exactly representable as a double, and the rounded complex product still gets thousands of them wrong.
def exact_product(a, b, bits=64):
"""The exact reference: pack each list into one Python integer and multiply."""
ia = sum(v << (bits * i) for i, v in enumerate(a))
ib = sum(v << (bits * i) for i, v in enumerate(b))
prod, mask = ia * ib, (1 << bits) - 1
return [(prod >> (bits * i)) & mask for i in range(len(a) + len(b) - 1)]
rng = random.Random(2025)
wrong = {}
for n in (512, 2048, 8192):
x = [rng.randrange(10 ** 6) for _ in range(n)]
y = [rng.randrange(10 ** 6) for _ in range(n)]
exact = exact_product(x, y)
assert max(exact) < 2.1e15 < 2 ** 53
wrong[n] = sum(g != e for g, e in zip(multiply(x, y), exact))
assert wrong == {512: 0, 2048: 0, 8192: 2239}
The failure is not a bug in the code; the same lists multiplied mod would still be wrong, but for the other reason (these coefficients exceed ). A floating-point FFT can be made safe by splitting numbers into small enough pieces that a proven error bound stays below .
Measure the claim
Multiplications for two lists of coefficients, counted by the code above (the transform column matches exactly):
| schoolbook | transform | transform | ||
|---|---|---|---|---|
| 16 | 32 | 256 | 304 | 1.90 |
| 32 | 64 | 1,024 | 704 | 1.83 |
| 64 | 128 | 4,096 | 1,600 | 1.79 |
| 1,024 | 2,048 | 1,048,576 | 37,888 | 1.68 |
| 4,096 | 8,192 | 16,777,216 | 176,128 | 1.65 |
The last column falls towards , since the ratio is exactly : the growth is with constant , while the schoolbook column grows sixteenfold for every fourfold step in . Rounded complex products of random entries below were exact at and and wrong in 2,239 of 16,383 coefficients at (one seed; a measurement, not a theorem).
assert [round(product_cost(n) / (2 * n * math.log2(2 * n)), 2) for n in (16, 32, 64, 1024, 4096)] \
== [1.90, 1.83, 1.79, 1.68, 1.65]
assert all(abs(product_cost(n) / (2 * n * math.log2(2 * n)) - (1.5 + 2 / math.log2(2 * n))) < 1e-12
for n in (16, 4096))
assert product_cost(4096) == 176_128
In real software
GMP, the GNU multiple-precision library, multiplies large integers with a sequence of algorithms
by size: schoolbook, Karatsuba, several Toom–Cook variants, and at the largest sizes an FFT in the
style of Schönhage and Strassen. That FFT works exactly, modulo , not in floating
point. CPython's int does not use an FFT: once both factors have more than 70 of its internal 30-bit
digits (140 when squaring), it switches from the schoolbook method to Karatsuba's algorithm. NumPy's numpy.fft computes floating-point complex
DFTs with the pocketfft library, so a product computed through it rounds, as ours did.
A problem that looks different
Two lists each hold 100,000 integers between 0 and 1,000,000. For every target you want to know how many pairs, one number from each list, add up to . A double loop makes additions. Is there a polynomial hiding in this question? (Not solved here; module 32 takes up questions of this kind, and the lab's last problem is a different one.)
Practise
In the lab you evaluate a polynomial by splitting it once and joining the halves with butterflies, watching each pair of outputs appear; predict and then trace where every coefficient lands when a list is split all the way down; write the iterative transform modulo a prime, its inverse and an exact product; count the multiplications of a product and derive the constant yourself; and finish with a problem that doesn't say what it is.
Recap
- You can now: multiply polynomials and big integers in arithmetic operations by evaluating at roots of unity, multiplying pointwise and interpolating with and ; explain the halving lemma, the butterfly and bit-reversed order; and say when the result is exact.
- Invariant: a length- DFT is the two length- DFTs of the even and odd coefficients, joined by butterflies .
- Complexity achieved: exactly multiplications per transform and per product ( a power of two), worst case, counting operations on exact complex numbers or residues mod ; against for the schoolbook method.
- Failure mode: too few points (the product wraps around into a cyclic convolution), a forgotten , trusting the result mod when coefficients reach , or trusting rounded floating point on large inputs.
- In real software: GMP's largest multiplications use a Schönhage–Strassen FFT modulo
; CPython's
intstops at Karatsuba;numpy.fftis floating point (pocketfft). - Next: module 32 asks what else a convolution computes, starting from the question above; module 24 returns to polynomials, and to why their values are such good fingerprints.
Check yourself
- Why do the -th roots of unity make the recursion work, when evaluating at would not? Point to the line of the argument that needs .
- Replace the transform mod by complex floating point and round at the end. When does it break, and what would you change to make it safe for 100,000-digit integers?
- You need the product of a polynomial with 100,000 coefficients and one with 5. Schoolbook or transform? Count both.
Topic list modeled on the public schedule of CMU 15-451/651 Algorithm Design and Analysis (Fall 2025). All lecture text, proofs, examples, code, and exercises are original to SciMigo; no CMU course materials are reproduced. This course is independent and not affiliated with or endorsed by Carnegie Mellon University.