Kronecker's factorization method in Python
Factors univariate integer polynomials by interpolating at integer points and searching divisors.
- File
kronecker_factorization_method.py, 132 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/kronecker-factorization-method/. The authors are listed in prov/9JH5VC.jsonl.
"""Kronecker's method: factor a primitive polynomial in Z[x] into irreducibles.
A polynomial is a list of integers [a_0, ..., a_d], lowest degree first, with a
nonzero last entry. The method is exponential in the degree and is meant for
small inputs and for teaching.
"""
from fractions import Fraction
from itertools import product
from math import gcd
from typing import List, Optional
Poly = List[int]
def evaluate(f: Poly, a: int) -> int:
v = 0
for c in reversed(f):
v = v * a + c
return v
def divisors(n: int) -> List[int]:
"""The positive divisors of n != 0, by trial division."""
n = abs(n)
small, large = [], []
i = 1
while i * i <= n:
if n % i == 0:
small.append(i)
if i * i != n:
large.append(n // i)
i += 1
return small + large[::-1]
def exact_quotient(f: Poly, g: Poly) -> Optional[Poly]:
"""f / g if g divides f in Z[x], else None."""
r = list(f)
q = [0] * (len(f) - len(g) + 1)
for s in range(len(f) - len(g), -1, -1):
c, m = divmod(r[s + len(g) - 1], g[-1])
if m:
return None
q[s] = c
for i, b in enumerate(g):
r[s + i] -= c * b
return q if not any(r) else None
def interpolate(points: List[int], values: List[int]) -> Optional[Poly]:
"""The polynomial of degree < len(points) through the points, if it is in Z[x]."""
n = len(points)
coeffs = [Fraction(0)] * n
for i in range(n):
basis = [Fraction(1)] # prod_{j != i} (x - a_j) / (a_i - a_j)
for j in range(n):
if j != i:
den = points[i] - points[j]
nxt = [Fraction(0)] * (len(basis) + 1)
for k, c in enumerate(basis):
nxt[k] -= c * points[j] / den
nxt[k + 1] += c / den
basis = nxt
for k in range(n):
coeffs[k] += values[i] * basis[k]
if any(c.denominator != 1 for c in coeffs):
return None
g = [int(c) for c in coeffs]
while g and g[-1] == 0:
g.pop()
return g
def choose_points(f: Poly, count: int) -> List[int]:
"""count integers with f(a) != 0, preferring values with few divisors."""
cands = []
a = 0
while len(cands) < 2 * len(f) + count:
for b in ((a,) if a == 0 else (a, -a)):
if evaluate(f, b) != 0:
cands.append(b)
a += 1
cands.sort(key=lambda b: (len(divisors(evaluate(f, b))), abs(b)))
return cands[:count]
def find_factor(f: Poly, e: int) -> Optional[Poly]:
"""A factor of f of degree exactly e, or None if there is none."""
pts = choose_points(f, e + 1) # @line L3
divs = [divisors(evaluate(f, a)) for a in pts] # @line L4
choices = [divs[0]] + [d + [-x for x in d] for d in divs[1:]] # g(a_0) > 0
for vals in product(*choices): # @line L5
g = interpolate(pts, list(vals)) # @line L6
if g is not None and len(g) == e + 1: # @line L7
q = exact_quotient(f, g)
if q is not None:
return [-c for c in g] if g[-1] < 0 else g
return None
def kronecker_factor(f: Poly) -> List[Poly]:
"""The irreducible factors of a primitive f with positive leading coefficient.
The product of the returned factors is f. Each factor has a positive
leading coefficient. Constants give the empty list.
"""
f = list(f)
while f and f[-1] == 0:
f.pop()
if not f:
raise ValueError("f must be nonzero")
c = 0
for a in f:
c = gcd(c, a)
if c != 1 or f[-1] < 0:
raise ValueError("f must be primitive with a positive leading coefficient")
factors: List[Poly] = []
e = 1 # @line L1
while 2 * e <= len(f) - 1: # @line L2
g = find_factor(f, e)
if g is None:
e += 1 # @line L9
else:
factors.append(g) # @line L8
f = exact_quotient(f, g)
if len(f) > 1:
factors.append(f) # @line L10
return factors # @line L11
test_kronecker_factorization_method.py 100 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/kronecker-factorization-method/. The authors are listed in prov/9JH5VC.jsonl.
import random
from fractions import Fraction
from math import gcd
import pytest
from kronecker_factorization_method import divisors, evaluate, exact_quotient, interpolate, kronecker_factor
def mul(a, b):
r = [0] * (len(a) + len(b) - 1)
for i, x in enumerate(a):
for j, y in enumerate(b):
r[i + j] += x * y
return r
def primitive(f):
c = 0
for a in f:
c = gcd(c, a)
if f[-1] < 0:
c = -c
return [a // c for a in f]
def has_rational_root(f):
"""Rational root test: a root p/q has p | a_0 and q | lc."""
if f[0] == 0:
return True
for p in divisors(f[0]):
for q in divisors(f[-1]):
for s in (1, -1):
r = Fraction(s * p, q)
if sum(c * r ** i for i, c in enumerate(f)) == 0:
return True
return False
def check(f):
fs = kronecker_factor(f)
prod = [1]
for g in fs:
assert g[-1] > 0 and len(g) >= 2
prod = mul(prod, g)
assert prod == f
for g in fs:
if len(g) - 1 in (2, 3): # degree 2 or 3: irreducible iff no rational root
assert not has_rational_root(g)
return fs
def test_worked_example():
assert check([4, 0, 0, 0, 1]) == [[2, -2, 1], [2, 2, 1]] # x^4 + 4
def test_known_factorizations():
assert sorted(check([-1, 0, 0, 0, 0, 0, 1])) == sorted([[-1, 1], [1, 1], [1, -1, 1], [1, 1, 1]])
assert check([1, 0, 1]) == [[1, 0, 1]]
assert check([-2, 0, 1]) == [[-2, 0, 1]]
assert check([1, 1, 1, 1, 1]) == [[1, 1, 1, 1, 1]] # cyclotomic, irreducible
assert check([0, 1]) == [[0, 1]]
assert check([0, 0, 0, 1]) == [[0, 1], [0, 1], [0, 1]]
assert check([1, 2, 1]) == [[1, 1], [1, 1]]
assert check([3, 2]) == [[3, 2]]
assert kronecker_factor([1]) == []
def test_random_products():
rng = random.Random(1882)
for _ in range(500):
f = [1]
for _ in range(rng.randint(1, 3)):
deg = rng.randint(1, 2)
g = [rng.randint(-3, 3) for _ in range(deg + 1)]
if g[-1] == 0:
g[-1] = 1
f = mul(f, g)
f = primitive(f)
check(f)
def test_interpolation_and_division():
rng = random.Random(5)
for _ in range(500):
g = [rng.randint(-5, 5) for _ in range(rng.randint(1, 4))] + [rng.randint(1, 3)]
pts = list(range(-2, len(g) - 2))
assert interpolate(pts, [evaluate(g, a) for a in pts]) == g
h = [rng.randint(-3, 3) for _ in range(3)] + [1]
assert exact_quotient(mul(g, h), g) == h
def test_bad_input():
with pytest.raises(ValueError):
kronecker_factor([2, 4])
with pytest.raises(ValueError):
kronecker_factor([1, -1])
What the next level needs
- the tests did not run in this build