Appendix D: Solutions for Chapter 2
This page collects solutions and editorial notes for the exercises in Chapter 2: Mathematical preliminaries. Compute and derivation exercises have worked solutions; open-ended exercises have an editorial note describing what a strong answer addresses.
Every block below is self-contained, so a block that uses a chapter helper restates it at the top. The fuller versions of these routines, with the reasoning written out, are in the prelim_algebra package under solutions/ch02-algebra. From a clone of the companion repository, pytest tests/ch02 runs its suite. Appendix C has the setup.
Exercise 1
Section titled “Exercise 1”Both routes reduce to 3 modulo 13. Reducing the exponent modulo 12 first is the standard application of Fermat’s little theorem. With the exponent already reduced from 200 to 8, the second computation is a sanity check rather than a separate calculation. The exercise asks for the chapter’s mod_pow rather than the built-in, so the block restates it and then confirms the built-in agrees.
def mod_pow(base, exponent, modulus): assert exponent >= 0, "exponent must be non-negative" assert modulus >= 1, "modulus must be at least 1" result = 1 % modulus base = base % modulus while exponent > 0: if exponent % 2 == 1: result = (result * base) % modulus base = (base * base) % modulus exponent //= 2 return result
direct = mod_pow(7, 200, 13)reduced = mod_pow(7, 200 % 12, 13)print(direct, reduced)print(direct == reduced == pow(7, 200, 13))# ==> 3 3# ==> TrueExercise 2
Section titled “Exercise 2”The candidate fails because , so the order of 2 is 8, not 16, and the orbit covers only half of the group. The next candidate has order exactly 16 and is a generator. The full power sequence is shown below. Verify that the multiset matches .
Order always divides by Lagrange’s theorem, which is why comparing the order against is the whole generator test and no separate check is needed.
def order(g, p): x, k = g % p, 0 while True: k += 1 if x == 1: return k x = (x * g) % p
def find_generator(p): for candidate in range(2, p): if order(candidate, p) == p - 1: return candidate raise AssertionError(f"no generator found modulo {p}")
powers_of_3 = [pow(3, k, 17) for k in range(1, 17)]print(order(2, 17), order(3, 17))print(find_generator(17))print(powers_of_3)# ==> 8 16# ==> 3# ==> [3, 9, 10, 13, 5, 15, 11, 16, 14, 8, 7, 4, 12, 2, 6, 1]Exercise 3
Section titled “Exercise 3”The values are . None is zero modulo 7, so has no root in . Because has degree 3, the equivalence cited in the exercise applies: a proper factorization of a degree-3 polynomial over a field would include a linear factor, and a linear factor would force a root. There is no root, so there is no proper factorization, so is irreducible over .
Returning the list of roots rather than a boolean costs nothing and says more: the empty list is the irreducibility witness here, and on a polynomial that does factor the same call names the linear factors.
def poly_eval(coeffs, x, p): return sum(c * pow(x, i, p) for i, c in enumerate(coeffs)) % p
def roots(coeffs, p): return [k for k in range(p) if poly_eval(coeffs, k, p) == 0]
# f(x) = 1 + x + 0*x^2 + 1*x^3coeffs = [1, 1, 0, 1]print([poly_eval(coeffs, k, 7) for k in range(7)])print(roots(coeffs, 7))# ==> [1, 3, 4, 3, 6, 5, 6]# ==> []Exercise 4
Section titled “Exercise 4”The rank is 2. Reducing modulo 7 sends the entry 7 in row 4 to 0, but the structural argument does not depend on that detail. Every row is an arithmetic progression with common difference 1, so successive rows differ by the all-ones vector . That gives row row 1 , so every row lies in . The two spanning vectors are linearly independent because no scalar multiple of equals row 1, so the row space has dimension exactly 2.
The same conclusion can be reached from the other side. The rows satisfy two independent relations, and , which show the rank is at most 2. The span argument above shows it is at least 2. Together they pin it at exactly 2. Exhibiting relations alone would not be enough: two relations bound the rank from above and leave open whether a third exists.
def gauss_eliminate(matrix, p): assert p > 1, "p must be prime; this helper does not test primality" m = [row[:] for row in matrix] rows = len(m) cols = len(m[0]) if m else 0 rank = 0 for col in range(cols): pivot = None for r in range(rank, rows): if m[r][col] % p != 0: pivot = r break if pivot is None: continue m[rank], m[pivot] = m[pivot], m[rank] inv = pow(m[rank][col], p - 2, p) m[rank] = [(x * inv) % p for x in m[rank]] for r in range(rows): if r != rank and m[r][col] % p != 0: factor = m[r][col] m[r] = [(m[r][c] - factor * m[rank][c]) % p for c in range(cols)] rank += 1 return m, rank
M = [[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]]_reduced, rank = gauss_eliminate(M, 7)print("rank =", rank)
ones = [1, 1, 1, 1]diff = [[(M[i][j] - M[i - 1][j]) % 7 for j in range(4)] for i in range(1, 4)]print(all(row == ones for row in diff))
r1, r2, r3, r4 = Mprint([r3[j] - 2 * r2[j] + r1[j] for j in range(4)])print([r4[j] - 2 * r3[j] + r2[j] for j in range(4)])# ==> rank = 2# ==> True# ==> [0, 0, 0, 0]# ==> [0, 0, 0, 0]