Polynomials in Algorithm Design
Is this product right?
A library hands back the product of two polynomials, computed with module 25's fast Fourier transform: in floating point, which rounds, or modulo a prime, which wraps large coefficients. You hold three coefficient lists and one question: is ?
Here is a small instance, coefficients listed from up:
and the claimed product is [8, 29, 21, 53, 22, 17, 6].
A = [2, 7, 1, 3]
B = [4, 1, 5, 2]
C = [8, 29, 21, 53, 22, 17, 6] # the claimed product
AB = [sum(A[i] * B[k - i] for i in range(4) if 0 <= k - i < 4) for k in range(7)]
assert AB == [8, 30, 21, 52, 22, 17, 6] # the true product
assert [c - t for c, t in zip(C, AB)] == [0, -1, 0, 1, 0, 0, 0]
assert sum(C) == sum(AB) == 156 # the coefficient sums agree
Two coefficients are off by one, in opposite directions, the kind of damage rounding leaves.
The naive approaches. Recomputing the product costs multiplications for polynomials with coefficients, or with module 25, and it trusts a second implementation to check the first, when we need one bit. The cheap alternative is a checksum: compare the sum of 's coefficients with the product of the sums of 's and 's. That compares with , and here it says and passes a wrong answer. A check at a fixed point is blind to every error that vanishes there.
The fix is a random point. This module proves why that works, then uses the same fact to recover lost data and to test a graph for a perfect matching.
Arithmetic in a field
We compute modulo a prime . The set with addition and multiplication mod is a field: every nonzero has an inverse, by Fermat's little theorem, so division works. Arithmetic is exact, and each number stays below . Costs count field operations (additions, subtractions, multiplications mod ), each on a word-RAM when fits in a word; an inverse costs of them. Every error bound below holds for every input, over the algorithm's own coins.
A polynomial of degree is stored as its coefficient list . Addition costs field operations, and evaluation costs multiplications by Horner's rule, . Multiplying two polynomials with coefficients costs multiplications by the schoolbook method. Module 25 does it in field operations when the field has a primitive -th root of unity for a power of two (such as its prime 998244353); modulo other primes, one uses a few such primes and the Chinese remainder theorem.
Why a prime? Modulo 8, which is not a field, : two nonzero numbers multiply to zero. The lemma below then fails.
assert [x for x in range(8) if (x * x - 1) % 8 == 0] == [1, 3, 5, 7] # degree 2, four roots
The one lemma
Root bound
A nonzero polynomial of degree over a field has at most roots.
Proof. By induction on . A nonzero constant has no roots. If has degree and a root , divide by : , with . If is another root, then . In a field a product is zero only if a factor is, and , so . Every root other than is a root of , which has at most by induction.
The step not to skip is "a product is zero only if a factor is". Modulo 8 it is false, and vanishes at 3 because .
The consequence every algorithm here uses. If and both have degree at most , then is nonzero of degree at most , so and agree on at most points. Draw uniformly from a set of field elements:
The error is one-sided. If , then certainly . Only "equal" can be wrong.
Tracing the check
For the product, and , both of degree at most 6. Take so the whole field fits in a picture. The check draws , computes , and with three evaluations, and compares with . With : , , , but . The claim is wrong, and proves it.
def ev(c, x, p): # a stand-in: the lab asks for Horner's rule
return sum(ci * pow(x, i, p) for i, ci in enumerate(c)) % p
p = 13
row_AB = [ev(A, r, p) * ev(B, r, p) % p for r in range(p)]
row_C = [ev(C, r, p) for r in range(p)]
assert row_AB == [8, 0, 2, 5, 6, 4, 0, 1, 2, 11, 0, 3, 10]
assert row_C == [8, 0, 8, 3, 1, 7, 2, 12, 12, 3, 2, 10, 10]
fooled = [r for r in range(p) if row_AB[r] == row_C[r]]
assert fooled == [0, 1, 12]
assert all((r ** 3 - r) % p == 0 for r in fooled) # C − AB = x³ − x
The points that fool the check are exactly the roots of . There are three of them in every field, whatever is: 0 (which compares constant terms), 1 (the checksum) and . With the check misses with probability , and the lemma promises at most for any wrong of degree at most 6. Three evaluations cost multiplications for coefficients in and : each for and , for , and one for the product.
Predict: the library computed exactly, modulo 998244353, but some true coefficients were larger than that prime. You check with a random r, also modulo 998244353. Will you catch it?
Never. The library's output is the true product reduced mod 998244353, so as polynomials over that field and every agrees. A check must not share the blind spot of the computation it checks. Work modulo a different prime , larger than every coefficient of and of the true product: then a nonzero over the integers stays nonzero mod .
Many variables: Schwartz–Zippel and Freivalds
Schwartz–Zippel lemma
Let be a nonzero polynomial of total degree over a field, a finite set of field elements, and drawn independently and uniformly from . Then .
Proof sketch. Induct on ; is the root bound. Write as a polynomial in whose coefficients are polynomials in the other variables, and let be the top term with , so . Either , with probability at most by induction, or it is nonzero, and then is a nonzero polynomial of degree in , which vanishes at with probability at most . Add the two.
Freivalds' check. A service claims for matrices. Recomputing takes multiplications by the schoolbook method, and no known algorithm multiplies matrices in . Instead draw and compare with : three matrix–vector products, multiplications. If , some row of is nonzero, and is a nonzero polynomial of degree 1 in . By Schwartz–Zippel it vanishes with probability at most , so a wrong passes with probability at most per round, and independent rounds miss with probability at most . A correct always passes.
M = [[2, 1, 3], [0, 4, 1], [5, 2, 2]]
N = [[1, 3, 0], [2, 1, 4], [3, 0, 1]]
claimed = [[13, 7, 7], [11, 4, 18], [15, 17, 10]] # row 1, column 2 (from 0) is wrong
times = lambda X, v: [sum(a * b for a, b in zip(row, v)) for row in X]
true = [[sum(M[i][k] * N[k][j] for k in range(3)) for j in range(3)] for i in range(3)]
assert true[1][2] == 17 and claimed[1][2] == 18
r = [1, 2, 3]
assert times(N, r) == [7, 16, 6]
assert times(M, times(N, r)) == [48, 70, 79] and times(claimed, r) == [48, 73, 79]
assert times(M, times(N, [5, 1, 0])) == times(claimed, [5, 1, 0]) # r[2] = 0 misses it
Counting rows, columns and coordinates from 0, the wrong entry is in row 1, column 2, and row 1 of the results differs by 3: the error times . Any with misses it, which is why must be drawn from a large set: from the miss probability can be . At the check costs multiplications per round, against to recompute.
Two representations
A polynomial of degree less than is fixed by its coefficients, or by its values at any distinct points. Lagrange interpolation goes from values back to coefficients:
Each is 1 at and 0 at the other points, so the sum takes the value at . It is the only such polynomial: two of them would differ by a polynomial of degree less than with roots, which the root bound forces to be zero.
Costs, in field operations: Lagrange takes if you form once and divide out one factor per basis polynomial, plus inverses; building each from scratch costs . Evaluating at points by Horner is . At the -th roots of unity, module 25 goes both ways in ; at arbitrary points is known when module 25's transform is available, with a tree of products and fast division (cited). In value form, multiplication is pointwise.
Reed–Solomon codes: lose half, lose nothing
Store a message over as the polynomial , and hand out six shares for . That is a Reed–Solomon code with and . The shares need distinct points, so .
Suppose shares 2, 4 and 5 are lost. From the kept points the Lagrange
denominators are , and , with inverses
68, 16 and 13 mod 97. The basis polynomials come out as [60, 67, 68], [96, 82, 16] and
[39, 45, 13], and is [42, 7, 19].
q = 97
shares = [(x, ev([42, 7, 19], x, q)) for x in range(1, 7)]
assert shares == [(1, 68), (2, 35), (3, 40), (4, 83), (5, 67), (6, 89)]
kept = {1: 68, 3: 40, 6: 89}
basis = {1: [60, 67, 68], 3: [96, 82, 16], 6: [39, 45, 13]}
assert [10 * 68 % q, 91 * 16 % q, 15 * 13 % q] == [1, 1, 1] # the inverses
assert all(ev(basis[i], j, q) == (i == j) for i in kept for j in kept)
message = [sum(kept[i] * basis[i][t] for i in kept) % q for t in range(3)]
assert message == [42, 7, 19]
assert all(ev(message, x, q) == y for x, y in shares) # the lost shares return too
Three properties follow from the lemma.
- Erasures. Any shares determine , so up to lost shares are recovered.
- Distance. Two different messages agree on at most points, so their share lists differ in at least places. Here that is 4.
- Errors. With distance , up to corrupted shares can be corrected: only one message is that close. Here that is one share. Berlekamp and Welch's algorithm does it in polynomial time (stated, not proved here).
If is a power of two dividing and the points are the -th roots of unity, encoding is one transform of module 25, .
Polynomials and matchings
Module 30 decided whether a graph has a perfect matching with Edmonds' blossom algorithm. A determinant does it too. For a graph on vertices , give each edge with a variable , and build the Tutte matrix : , , and 0 where there is no edge. It is skew-symmetric, .
Theorem (Tutte, 1947)
has a perfect matching if and only if is not the zero polynomial.
Proof sketch. Expand as a sum over permutations of . A fixed point contributes a zero diagonal entry. Take a permutation with an odd cycle, and reverse the odd cycle through the smallest vertex that lies on any odd cycle. The result has the same sign, and skew-symmetry turns its product into the original times , so the two terms cancel, and reversing again gives back the first. Every surviving term uses only even cycles, and the edges of an even cycle split into two matchings of its vertices, so a nonzero yields a perfect matching. Conversely, a perfect matching gives the permutation that swaps each pair of . Its term is , and no other permutation produces that monomial, so nothing cancels it.
has degree . Lovász's test substitutes values drawn uniformly from and computes the determinant by Gaussian elimination, field operations. A nonzero value proves that a perfect matching exists. A zero is wrong with probability at most , by Schwartz–Zippel.
The graph below is two triangles joined by the edge 2–3. Its only perfect matching is . Remove the edge 2–3 to get : two triangles, odd pieces, no perfect matching, and for every substitution.
The block below computes determinants by the permutation expansion itself (a stand-in, fine for six vertices; the test uses elimination), and checks both the numbers and the cancellation:
from itertools import permutations
import random
def sign(s):
seen, flips = set(), 0
for i in range(len(s)):
j, length = i, 0
while j not in seen:
seen.add(j)
j, length = s[j], length + 1
flips += max(length - 1, 0)
return -1 if flips % 2 else 1
def odd_cycle(s):
seen = set()
for i in range(len(s)):
j, length = i, 0
while j not in seen:
seen.add(j)
j, length = s[j], length + 1
if length % 2:
return True
return False
def tutte(n, values, p):
T = [[0] * n for _ in range(n)]
for (i, j), x in values.items():
T[i][j], T[j][i] = x % p, -x % p
return T
def det_terms(T, p, keep=lambda s: True):
total = 0
for s in permutations(range(len(T))):
if keep(s):
term = sign(s)
for i in range(len(T)):
term = term * T[i][s[i]]
total += term
return total % p
G1 = {(0, 1): 3, (0, 2): 5, (1, 2): 7, (2, 3): 2, (3, 4): 4, (3, 5): 6, (4, 5): 8}
assert det_terms(tutte(6, G1, 101), 101) == 82 == (3 * 2 * 8) ** 2 % 101
rng = random.Random(2401)
for _ in range(20):
vals = {e: rng.randrange(101) for e in G1}
T = tutte(6, vals, 101)
assert det_terms(T, 101, keep=odd_cycle) == 0 # odd cycles cancel
assert det_terms(T, 101) == (vals[0, 1] * vals[2, 3] * vals[4, 5]) ** 2 % 101
G2 = {e: x for e, x in vals.items() if e != (2, 3)}
assert det_terms(tutte(6, G2, 101), 101) == 0
The second-to-last assert tests an identity between two polynomials, and , at twenty random points: the method of this module, used on itself.
Which answer can be wrong. "Yes" is always right. "No" can be wrong: for the determinant is zero exactly when one of is 0. Module 30's final forest proves a "no" with a Tutte–Berge set that anyone can check; a zero determinant proves nothing, and it does not name a matching either. One way to find one: delete each edge in turn and keep the deletion whenever the determinant stays nonzero. A nonzero value is never wrong, so no needed edge is deleted, and with probability at least (a union bound over the tests) the edges left are exactly a perfect matching. Faster algebraic methods run in with fast matrix multiplication (Mucha and Sankowski, 2004, cited). The rank of at a random point is twice the size of a maximum matching with probability at least (Lovász, 1979, cited).
Complexity
All costs count field operations mod , worst case over inputs; error bounds are over the algorithm's coins, for every input.
- Product check for polynomials with coefficients and a claimed of degree at most : multiplications per round, false "equal" with probability at most . Recomputing: , or with module 25.
- Freivalds: multiplications per round, false "equal" at most per round. Recomputing: by the schoolbook method; the best known exponent is below 2.372, still above 2.
- Interpolation of points: , plus inverses.
- Tutte test: per trial, false "no" at most ; "yes" is never wrong.
A deterministic checker must read all entries of on a correct input: changing an unread entry would leave its run, and its "yes", unchanged. Freivalds reads each entry once per round.
Measure the claim
How close is the bound for ? Its determinant is , which is zero exactly when one of three independent uniform values is 0:
| false "no" per trial | bound | |
|---|---|---|
| 5 | 0.488 | 1.2 |
| 11 | 0.2487 | 0.545 |
| 101 | 0.0294 | 0.0594 |
def false_no(p): # triples of values for the matching edges with a zero product
return sum(a * b * c % p == 0 for a in range(p) for b in range(p) for c in range(p)) / p ** 3
assert false_no(5) == 61 / 125 == 0.488
assert round(false_no(11), 4) == 0.2487 and round(false_no(101), 4) == 0.0294
P = 2 ** 31 - 1
assert round((P ** 3 - (P - 1) ** 3) / P ** 3 * 1e9, 2) == 1.40 and round(6 / P * 1e9, 2) == 2.79
assert all(false_no(p) <= 3 / p for p in (5, 11, 101)) and (200 / P) ** 2 < 1e-14
At the bound exceeds 1 and says nothing; the lemma needs well above the degree. Here the truth is below half the bound. That is no accident: is the square of a polynomial of degree , the Pfaffian of (cited, not proved here), so Schwartz–Zippel applied to the Pfaffian gives , which is 0.6 at . The bound is a guarantee for every graph, not a prediction for this one. Two trials at push the error for any 200-vertex graph below .
A problem that looks different
A vault code must be split among five officers so that any three of them can open the vault together, while any two, pooling everything they hold, learn nothing at all about the code. What should each officer be given, and where in it would the code live? The lab's last problem is a different one.
Practise
In the lab you write the polynomial operations and count roots with and without a field; trace the recovery of a message from three shares; test graphs for perfect matchings and extract one; count Freivalds' multiplications and how often it misses; and settle a question between two sites that doesn't say what it is.
Recap
You can now: prove the root bound and say where it needs a field; bound the one-sided error of a random-evaluation test with Schwartz–Zippel; check a polynomial or matrix product without recomputing it; recover Reed–Solomon shares by interpolation; and test for a perfect matching with one random determinant.
Invariant: two different polynomials of degree at most over a field agree on at most points, so a uniform point of a set exposes the difference with probability at least .
Complexity achieved: a product of matrices checked in multiplications per round (error at most ) instead of ; a polynomial product in instead of ; perfect matching in field operations per trial, false "no" at most .
Failure mode: arithmetic modulo a composite, a random set too small for the degree, or a check that shares the computation's modulus.
In real software: QR codes carry Reed–Solomon error correction; the ZXing library's QR encoder
computes it with ReedSolomonEncoder over the field GenericGF.QR_CODE_FIELD_256. Linux software
RAID 6 computes its second parity block (Q) by evaluating the data blocks as a polynomial over
at the point 2 by Horner's rule (lib/raid/raid6/int.uc), so any two lost
disks can be rebuilt.
Retrieval (module 05): Karp–Rabin compared two -bit numbers through their residues modulo a random prime. What bounded the error there, and what is random in today's test instead?
Check yourself
- Why does Freivalds' check miss a wrong with probability at most ? Which coordinate of carries the randomness in the lesson's example?
- Do all the arithmetic modulo instead of a prime, because it is faster. Which step of the root bound breaks, and how often could Freivalds' check then miss an error of ?
- A 200-vertex graph: compare Lovász's determinant test with module 30's blossom algorithm. What does each return, how can each be wrong, and when does the difference matter?
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.