On this page

The Fast Fourier Transform

Sixteen multiplications for one product

Multiply two polynomials:

A(x)=5+2x+7x2+3x3,

B(x)=4+6x2+x3.

Store each as its list of coefficients, lowest power first: a = [5, 2, 7, 3] and b = [4, 0, 6, 1]. The product has degree 6, and its coefficient of xk collects every pair of terms whose powers add up to k:

ck=∑i+j=kaibj.

This list c is called the convolution of a and b, written c=a*b. The schoolbook method computes it by multiplying every ai by every bj.

b0=4 b1=0 b2=6 b3=1 a0=5 a1=2 a2=7 a3=3 20 0 30 5 8 0 12 2 28 0 42 7 12 0 18 3
Every product a_i b_j, one cell each: 16 multiplications. The coefficient c_k is the sum of the anti-diagonal i + j = k; the highlighted one gives c_3 = 5 + 12 + 0 + 12 = 29. The product is c = [20, 8, 58, 29, 44, 25, 3].
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 n coefficients take n2 multiplications. That is 16 here and 1010 at n=100,000. The same sum appears whenever numbers are multiplied digit by digit: the integer 3725 is A(10) 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 n in O(nlogn) arithmetic operations.

Values multiply; coefficients don't

A polynomial can also be described by its values. Evaluate A and B at x=2: A(2)=61 and B(2)=36, so AB(2)=61·36=2196, one multiplication. Values multiply pointwise.

And values determine a polynomial. If two polynomials of degree less than N agree at N distinct points, their difference has degree less than N but N roots, so it is the zero polynomial. (That uses the fact that a nonzero polynomial of degree d has at most d roots, which holds over the complex numbers and over the integers modulo a prime; Polynomials in Algorithm Design proves it and builds much more on it.) AB has degree 6, so 7 of its values fix it. That suggests a plan:

  1. evaluate A and B at N≥7 points,
  2. multiply the values pointwise, N multiplications,
  3. interpolate: recover the 7 coefficients of AB from its N values.

For two inputs of length n, the product has 2n−1 coefficients, so N≥2n−1.

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 n multiplications per point, so evaluating at N arbitrary points costs about Nn, and interpolating back from arbitrary points costs Θ(N2) by Lagrange's formula. Each point is evaluated from scratch: nothing computed for x=2 helps with x=3. The rest of this module chooses the N 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 A into its even and odd parts: A(x)=(5+7x2)+x(2+3x2). At x=1 and x=−1 the two brackets take the same values, 12 and 5, because x2=1 for both. So A(1)=12+5=17 and A(−1)=12−5=7 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 p. 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 N that is a power of two (shorter inputs are padded with zeros, which at most doubles N). 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 N-th root of unity if ωN=1 and ωj≠1 for 0<j<N. Over the complex numbers, ω=e2πi/N, and its powers ω0,ω1,…,ωN−1 are N equally spaced points on the unit circle. The discrete Fourier transform (DFT) of a coefficient list (a0,…,aN−1) is the list of values

(A(ω0),A(ω1),…,A(ωN−1)).

Two facts about these points do all the work, for N even:

  • Halving lemma. ω2 is a primitive (N/2)-th root of unity. So squaring the N points gives the N/2 points of the half-size problem, each twice.
  • Opposite points. ωN/2=−1, so ωk+N/2=−ωk.

The second fact is where primitivity matters: (ωN/2)2=1 gives ωN/2=±1, and +1 is ruled out because N/2<N.

Now split A by the parity of its powers. Let E hold the even-indexed coefficients and O the odd-indexed ones, each a polynomial with N/2 coefficients, so that

A(x)=E(x2)+xO(x2).

At x=ωk and at x=ωk+N/2=−ωk the square is the same point ω2k. So for 0≤k<N/2:

A(ωk)=E(ω2k)+ωkO(ω2k),

A(ωk+N/2)=E(ω2k)−ωkO(ω2k).

The values E(ω2k) for k<N/2 are exactly the DFT of E at the (N/2)-th roots, and likewise for O. One product ωk·O(ω2k), one addition and one subtraction give two outputs. That pair of lines is called a butterfly.

Invariant

A DFT of length N is two DFTs of length N/2, of the even and the odd coefficients, joined by N/2 butterflies: output k and output k+N/2 are E±ωkO at the same point ω2k.

Recursing on E and O 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 p.

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 A with eight points

Pad a to length 8 (the product needs N≥7) and split it all the way down. Each level separates the even and odd positions of every piece.

0 4 0 4 2 6 2 6 0 2 4 6 1 5 1 5 3 7 3 7 1 3 5 7 0–7
Each node lists the indices of the coefficients it holds; its left child takes the even positions of that list and its right child the odd ones. For a, the left child of the root holds E = [5, 7, 0, 0] and the right child O = [2, 3, 0, 0]. The leaves, read left to right, are indices 0, 4, 2, 6, 1, 5, 3, 7.

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 j holds the coefficient whose index is j 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. E(y)=5+7y and O(y)=2+3y (the zeros drop out), evaluated at the fourth roots 1,i,−1,−i, which are ω2k for ω=e2πi/8 and k=0,1,2,3. Each row is one butterfly, giving outputs k and k+4.

k ω2k E(ω2k) O(ω2k) A(ωk) A(ωk+4)
0 1 12 5 17 7
1 i 5+7i 2+3i 4.29+10.54i 5.71+3.46i
2 −1 −2 −1 −2−i −2+i
3 −i 5−7i 2−3i 5.71−3.46i 4.29−10.54i

Row 2 is worth doing by hand: ω2=i, so A(i)=E(−1)+iO(−1)=−2−i, and indeed 5+2i−7−3i=−2−i. Row 0 is the pair 17,7 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 N. For N=1 the DFT of (a0) is A(ω0)=a0. For larger N, the recursive calls return the DFTs of E and O at the (N/2)-th roots (by the halving lemma, ω2 is the right root for them), and the two butterfly lines are the identity A(x)=E(x2)+xO(x2) at x=±ωk. The step that must not be skipped is ωN/2=−1: with a root that is not primitive, output k+N/2 is not the negated partner of output k, and the halves do not share their work.

The inverse is the same algorithm. Write the DFT as a matrix V with entries Vjk=ωjk. The product of V with the matrix of ω−1 has entries

∑k=0N−1ω(j−l)k.

If j=l every term is 1 and the sum is N. Otherwise the terms form a geometric series with ratio r=ωj−l≠1 (primitivity again), and its sum is (rN−1)/(r−1)=0 because rN=1. So the inverse of V is 1N times the matrix of ω−1: interpolate by running the FFT with ω−1 and dividing by N.

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 N coefficients. Here is the whole algorithm, over the complex numbers (rounded at the end) or modulo p:

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, N=4 for our pair, the product's seven coefficients do not fit: x4=1 at every fourth root of unity, so x4 folds onto x0, x5 onto x1, and so on. The result is the cyclic convolution ck+ck+4, [64, 33, 61, 29]. Forgetting to divide by N 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

Assume the required roots, the twiddle factors ωk, are precomputed; we count multiplications of transform values by those factors. Under this convention one length-N radix-2 transform performs exactly N2log2N such multiplications, as we now show. (The code above computes each factor with root, which the counter does not see.) Building the twiddle tables also costs arithmetic, but does not change the O(NlogN) bound.

Let T(N) count these multiplications for one transform. The two recursive calls cost T(N/2) each and the butterflies N/2, so T(N)=2T(N/2)+N/2 with T(1)=0. The recursion has log2N levels, each doing N/2 butterflies, so exactly

T(N)=N2log2N.

Compare Selection in Linear Time: median of medians satisfies T(n)≤T(n/5)+T(7n/10)+cn, whose subproblems add up to 9n/10, so the level sums shrink geometrically and the total is linear. Here the subproblems add up to exactly N, every level costs the same, and the logN factor stays.

A product makes two forward transforms, N pointwise products, one inverse transform and N scalings. With N≥2n−1 a power of two, and in the same convention, that is

3·N2log2N+2N

multiplications.

At n=100,000 the next power of two above 2n−1 is N=218, and the count is 7,602,176, against 1010 for the schoolbook method. For small n the schoolbook method wins: at n=16 it needs 256 multiplications and the transform 304. From n=32 on (1,024 against 704) the transform is cheaper. Space is O(N).

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 NlogN the end? For the DFT, O(NlogN) is the best known algorithm. No lower bound above linear is known in general arithmetic models. Morgenstern (1973) proved Ω(NlogN) 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 p have a primitive N-th root of unity exactly when N divides p−1. The prime p=998244353 is chosen for that: p−1=119·223, and 3 generates all of the nonzero residues, so ω=3(p−1)/N is a primitive N-th root for every power of two N≤223. The transform modulo p (often called the number-theoretic transform, NTT) makes no rounding error at all. It returns each ckmodp, which is ck itself whenever 0≤ck<p. Past that it is silently wrong as an integer: 400002 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 90417263×58820419. Their digit lists, least significant first, convolve to 15 sums of which the largest is 190, far below p, so the transform mod p 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 1/2. That error grows with the size of the coefficients and with the number of levels. Having every exact coefficient below 253 is not enough. Here are two random lists of 8,192 entries below 106: every coefficient of the product is below 2.1·1015, 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 p would still be wrong, but for the other reason (these coefficients exceed p). A floating-point FFT can be made safe by splitting numbers into small enough pieces that a proven error bound stays below 1/2.

Measure the claim

Multiplications for two lists of n coefficients, counted by the code above (the transform column matches 3·N2log2N+2N exactly):

n N schoolbook n2 transform transform /(Nlog2N)
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 3/2, since the ratio is exactly 3/2+2/log2N: the growth is NlogN with constant 3/2, while the schoolbook column grows sixteenfold for every fourfold step in n. Rounded complex products of random entries below 106 were exact at n=512 and n=2,048 and wrong in 2,239 of 16,383 coefficients at n=8,192 (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 2N′+1, 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 O(n1.59) 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 t you want to know how many pairs, one number from each list, add up to t. A double loop makes 1010 additions. Is there a polynomial hiding in this question? (Not solved here; Convolutions and Their Applications 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 O(nlogn) arithmetic operations by evaluating at roots of unity, multiplying pointwise and interpolating with ω−1 and 1/N; explain the halving lemma, the butterfly and bit-reversed order; and say when the result is exact.
  • Invariant: a length-N DFT is the two length-N/2 DFTs of the even and odd coefficients, joined by butterflies E(ω2k)±ωkO(ω2k).
  • Complexity achieved: exactly N2log2N multiplications per transform and 3·N2log2N+2N per product (N≥2n−1 a power of two), worst case, counting operations on exact complex numbers or residues mod p; against n2 for the schoolbook method.
  • Failure mode: too few points (the product wraps around into a cyclic convolution), a forgotten 1/N, trusting the result mod p when coefficients reach p, or trusting rounded floating point on large inputs.
  • In real software: GMP's largest multiplications use a Schönhage–Strassen FFT modulo 2N′+1; CPython's int stops at Karatsuba; numpy.fft is floating point (pocketfft).
  • Next: Convolutions and Their Applications asks what else a convolution computes, starting from the question above; Polynomials in Algorithm Design returns to polynomials, and to why their values are such good fingerprints.

Check yourself

  1. Why do the N-th roots of unity make the recursion work, when evaluating at 0,1,…,N−1 would not? Point to the line of the argument that needs ωN/2=−1.
  2. Replace the transform mod p 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?
  3. 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.

Your turn

Now try it yourself

Write the code, press Run, and scrub through the frames your program draws. The checks tell you when it works.

Loading the lab…

In this lab

  1. Evaluate at eight points by splitting once

    The lesson's invariant, one level deep: a list of N values of a polynomial at the N-th roots of unity is two lists of N/2 values of its halves, joined by butterflies. Here you do that one split by hand, in arithmetic modulo 17, where the numbers stay small enough to read. The prime 17 has a primitive 8th root of unity: w = 9, since 9^4 = 6561 leaves remainder 16 = −1 mod 17, so 9^8 leaves 1.

    Write dft_by_halves(a, w, p). It returns the values A(w^0), A(w^1), …, A(w^(N−1)) mod p, where a holds the N coefficients (N even). Split a into the even-indexed coefficients E and the odd-indexed ones O. Evaluate each half with the given evaluate(poly, x, p) at the N/2 points w^(2k) only, never the whole polynomial. Then, for each k < N/2, one butterfly gives two outputs: E(w^(2k)) + w^k·O(w^(2k)) at position k and E(w^(2k)) − w^k·O(w^(2k)) at position k + N/2, both mod p. Call show(...) after each butterfly.

    The starter is correct but evaluates the whole polynomial at all N points, N multiplications each. Yours makes N evaluations of halves instead: half the work, and the same answer. Before you press Run: the demo is A = 3 + x + 4x² + x³ + 5x⁴ + 9x⁵ + 2x⁶ + 6x⁷. For k = 0 the point is 1, so E(1) and O(1) are just sums of coefficients. Which two outputs does the first frame fill, and what are they mod 17?

  2. Where does every coefficient land?

    An iterative transform never splits a list at all. It puts the coefficients once in the order the recursion's leaves would have them, then combines neighbours. So you need to know that order, and you will find it twice: once by splitting, once from the bits of each index.

    Write split_levels(a), which returns every level of the even/odd split: level 0 is [a], and each next level replaces every piece by its even-indexed half followed by its odd-indexed half, until every piece has one entry. Call show(level, depth) once per level, so each level is one frame. Then write bit_reversed(N) for N a power of two with b bits: entry j is j with its b bits read backwards. Compute it from the bits of j, without splitting a list.

    The program splits the 16 letters abcdefghijklmnop, one frame per level. Answer the two questions first, on paper: the lesson did this for 8 coefficients, and 16 needs one more bit.

  3. The iterative transform, its inverse, and an exact product

    Now the transform itself, the way libraries write it: no recursion and no list slicing. Put the coefficients in bit-reversed order (exercise 2) once. Then make passes for L = 2, 4, 8, …, N: each pass combines two neighbouring blocks of length L/2, which already hold transforms, into one block of length L with L/2 butterflies, using w^(N/L) as the block's root. After the pass for L, every block of length L holds the L-point transform of the coefficients it came from. That is the lesson's invariant, and a check tests it on every frame.

    Write ntt(a, w, p, draw=False), which returns the values [A(w^0), …, A(w^(N−1))] mod p without changing a; with draw=True it calls show(a, L, p) after the reordering (L = 1) and after each pass. Write intt(values, w, p), which goes back: run ntt with the inverse root and multiply by 1/N, both computed mod p with Fermat's little theorem, pow(x, p − 2, p). Then write multiply(a, b), the exact product modulo P = 998244353 (P − 1 = 119·2²³, and 3 generates the nonzero residues, so w = 3^((P−1)/N) is a primitive N-th root for every power of two N up to 2²³).

    The starter evaluates each point from scratch, N² multiplications, and multiplies by the schoolbook method. A check hands your functions values that count their own multiplications, so it counts what the lesson counts: multiplications of the data by twiddle factors (and, in multiply, pointwise products and scalings), not the plain-int arithmetic that advances or builds the twiddle factors, which the lesson treats as precomputed. (A copy made with int() escapes the count; it is feedback, not security.) The demo draws your transform of exercise 1's polynomial, pass by pass, mod 17. Before you press Run: which frame will show the list [14, 13, 7, 11, 14, 1, 14, 1]?

  4. Count the multiplications, then find the constant

    The lesson proved that a product of two lists of n coefficients makes 3·(N/2)·log2 N + 2N multiplications, with N the smallest power of two at least 2n − 1, against n² for the schoolbook method. As in the lesson, the count is of multiplications of values: transform values by twiddle factors (the roots w^k), pointwise products and scalings. Treat the twiddle factors as precomputed: in the given ntt they are advanced with plain ints (t = t * wl % p), which Tally does not count. Building them costs extra arithmetic but does not change the O(N log N) bound. Count them yourself.

    Tally is a residue that adds one to Tally.count every time it takes part in a multiplication. The schoolbook method and exercise 3's product are given as schoolbook(a, b) and fast_product(a, b). Write count(mul, n, seed), which builds two lists of n Tally values with rng = random.Random(seed) (first list, then second, each entry rng.randrange(1000)), calls mul once on them, and returns the number of multiplications that call made. Then write profile(ns, seed), which returns one row (n, schoolbook count, transform count, transform count ÷ (N·log2 N)) per n.

    The program profiles n = 30, 120 and 480 and draws your counts divided by n: the schoolbook bars grow fourfold at each step, the transform's barely move. A check hands count products and lists of its own and compares with what it counts itself. Answer the two questions first: the first derives the constant the last column is heading for.

  5. Mastery challenge: restore the scrambled tape blocks

    The Halvorsen tape archive keeps customers' records on tape in blocks of N numbers, each a residue modulo P = 998244353, with N a power of two up to 16,384. Before a block is written it is scrambled with a key of the same length: stored entry i is the sum over all j of key[j] · data[(i − j) mod N], mod P, which is what the given scramble(data, key) computes. The archive only issues keys that can be undone. When a customer asks for a block back, the tape robot has to restore it while the customer waits, and a restore job can fetch a few hundred thousand blocks.

    Write restore(stored, key), which returns the data block. The starter is correct: it sets up the N equations and solves them by Gaussian elimination, about N³ steps, which for one block of 16,384 entries is more than 4 · 10¹² steps. Aim for a restore whose cost grows barely faster than N.

    Reading tape is slow, so every entry your code reads from stored or key counts. A check restores blocks of 4,096, 8,192 and 16,384 entries and allows 20 reads per entry of each list, and on the smallest it also counts the multiplications your code does with the entries; the starter runs out at once. (The count is feedback, not security: a copy of a list is charged once and free after that.)

    Answer the four questions, then write it. The demo restores block 3 of tape 7, 32 entries that are the bytes of a short label, and draws the stored block above the restored bytes.