wiki

Berlekamp's algorithm

Berlekamp's algorithm factors a square-free polynomial over a finite field using linear algebra. The residues modulo with form a vector space over , the Berlekamp subalgebra, whose dimension equals the number of irreducible factors of . It is the null space of , where is the matrix of the Frobenius map , so comes from one Gaussian elimination. Any element of the space that is not a constant splits through greatest common divisors[1]. The algorithm, published by Berlekamp in 1967, was the first polynomial-time factorization method for small fields, and together with Hensel lifting it made factorization of integer polynomials practical[7].

§ 01

Overview

Factoring a polynomial over or is usually done by factoring it modulo a prime and lifting the factors. Over there are only finitely many polynomials of each degree, so factoring by trial division is possible in principle but takes time exponential in the degree. Berlekamp's algorithm reduces the problem to linear algebra over , with a cost polynomial in the degree and in . The input is assumed square-free, which square-free decomposition arranges beforehand without any factoring.

§ 02

The Berlekamp subalgebra

Let be square-free with distinct monic irreducible factors of degrees . By the Chinese remainder theorem[10],

The Frobenius map is -linear on the ring on the left, since in characteristic and for . In each field its fixed points are exactly the elements of . So the fixed points in the whole ring are the residues that are constant modulo every :

The number of irreducible factors is therefore a dimension, and it can be computed without finding a single factor.

The matrix Q

On the basis , the Frobenius map has the matrix whose row holds the coefficients of . A residue , written as a row vector of coefficients, is fixed exactly when , so

The first row of is always zero, since the constant 1 is fixed, which accounts for the trivial solution. The matrix was considered by Petr in 1937[6], and is sometimes called the Petr–Berlekamp matrix.

§ 03

Splitting

Every is a vector of constants in the product decomposition, with . Since , is divisible by and[1][3]:

where the factor for is the product of the with . If is not a constant, two of its components differ, and at least two of the gcds are nontrivial. Running through a basis of and splitting the current factors with each basis vector separates all factors, because for any two factors some basis vector has different components on them.

§ 04

Example

Polynomials over F_p, as in the square-free decomposition article.

(* Polynomials over the prime field F_p, as arrays of coefficients,
constant term first, with no trailing zeros. *)
module Make (P : sig val p : int end) = struct
let p = P.p
let md x = ((x mod p) + p) mod p
let rec pw b e = if e = 0 then 1 else let h = pw (b * b mod p) (e / 2) in if e land 1 = 1 then h * b mod p else h
let inv a = pw (md a) (p - 2)
let trim a = let n = ref (Array.length a) in while !n > 0 && a.(!n - 1) = 0 do decr n done; Array.sub a 0 !n
let make l = trim (Array.of_list (List.map md l))
let deg a = Array.length a - 1
let zero = [||] and one = [| 1 |]
let x_plus c = make [ c; 1 ]
let lead a = a.(deg a)
let add a b = trim (Array.init (max (Array.length a) (Array.length b)) (fun i ->
md ((if i < Array.length a then a.(i) else 0) + if i < Array.length b then b.(i) else 0)))
let neg a = Array.map (fun c -> md (-c)) a
let sub a b = add a (neg b)
let scale k a = trim (Array.map (fun c -> md (k * c)) a)
let mul a b =
if a = zero || b = zero then zero else begin
let r = Array.make (Array.length a + Array.length b - 1) 0 in
Array.iteri (fun i x -> Array.iteri (fun j y -> r.(i + j) <- (r.(i + j) + x * y) mod p) b) a;
trim r end
let rec pow a e = if e = 0 then one else mul a (pow a (e - 1))
let divmod a b =
let q = Array.make (max 0 (deg a - deg b + 1)) 0 and r = Array.copy a in
let il = inv (lead b) in
for i = deg a - deg b downto 0 do
let c = r.(i + deg b) * il mod p in
q.(i) <- c;
Array.iteri (fun j y -> r.(i + j) <- md (r.(i + j) - c * y)) b
done;
(trim q, trim r)
let div a b = fst (divmod a b)
let rem a b = snd (divmod a b)
let monic a = if a = zero then a else scale (inv (lead a)) a
let rec gcd a b = if b = zero then monic a else gcd b (rem a b)
let deriv a = trim (Array.init (max 0 (Array.length a - 1)) (fun i -> md ((i + 1) * a.(i + 1))))
(* If a' = 0, every exponent is a multiple of p, and since c^p = c in
F_p, a is the p-th power of the polynomial with those coefficients. *)
let pth_root a = trim (Array.init (deg a / p + 1) (fun i -> a.(i * p)))
(* Printed with coefficients in (-p/2, p/2], highest degree first. *)
let to_string a =
if a = zero then "0" else
let signed c = if c > p / 2 then c - p else c in
let terms = List.rev (List.filter (fun (_, c) -> c <> 0) (List.mapi (fun i c -> (i, signed c)) (Array.to_list a))) in
let mono i c =
let k = abs c in
let cs = if k = 1 && i > 0 then "" else string_of_int k in
match i with 0 -> string_of_int k | 1 -> cs ^ "x" | _ -> cs ^ "x^" ^ string_of_int i in
String.concat "" (List.mapi (fun n (i, c) ->
(if n = 0 then (if c < 0 then "-" else "") else if c < 0 then " - " else " + ") ^ mono i c) terms)
end

The Q matrix, its null space, and splitting with gcd(g, v - s).

(* Berlekamp's algorithm over F_p, for monic square-free f. *)
module Make (P : sig val p : int end) = struct
include Fpoly.Make (P)
let rec powmod a e f = if e = 0 then one else
let h = powmod (rem (mul a a) f) (e / 2) f in
if e land 1 = 1 then rem (mul h a) f else h
(* Row i of Q holds the coefficients of x^(ip) mod f. *)
let q_matrix f =
let n = deg f in
let xp = powmod (make [ 0; 1 ]) p f in
let row = ref one in
Array.init n (fun _ ->
let r = Array.init n (fun j -> if j < Array.length !row then !row.(j) else 0) in
row := rem (mul !row xp) f; r)
(* A basis of { v : v (Q - I) = 0 }, by Gaussian elimination on the
transpose of Q - I. *)
let kernel q =
let n = Array.length q in
let a = Array.init n (fun i -> Array.init n (fun j -> md (q.(j).(i) - if i = j then 1 else 0))) in
let pivot_col = Array.make n (-1) in
let row = ref 0 in
for col = 0 to n - 1 do
match List.find_opt (fun r -> a.(r).(col) <> 0) (List.init (n - !row) (fun k -> !row + k)) with
| None -> ()
| Some r ->
let t = a.(r) in a.(r) <- a.(!row); a.(!row) <- t;
let iv = inv a.(!row).(col) in
a.(!row) <- Array.map (fun x -> x * iv mod p) a.(!row);
for r' = 0 to n - 1 do
if r' <> !row && a.(r').(col) <> 0 then begin
let k = a.(r').(col) in
a.(r') <- Array.mapi (fun j x -> md (x - k * a.(!row).(j))) a.(r')
end
done;
pivot_col.(!row) <- col; incr row
done;
let pivots = Array.to_list (Array.sub pivot_col 0 !row) in
let free = List.filter (fun c -> not (List.mem c pivots)) (List.init n Fun.id) in
List.map (fun fc ->
let v = Array.make n 0 in
v.(fc) <- 1;
List.iteri (fun r pc -> v.(pc) <- md (- a.(r).(fc))) pivots;
trim v) free
(* Split with gcd(g, v - s) for every s in F_p, until there are r factors. *)
let factor f =
let basis = kernel (q_matrix f) in
let r = List.length basis in
let factors = ref [ f ] in
List.iter (fun v ->
if deg v > 0 then
factors := List.concat_map (fun g ->
if List.length !factors >= r || deg g <= 1 then [ g ] else
let parts = List.filter (fun h -> deg h > 0)
(List.init p (fun s -> gcd g (sub v (make [ s ])))) in
if List.length parts > 1 then parts else [ g ]) !factors) basis;
(r, List.sort compare !factors)
end

Knuth's example over F_13.

module B = Berl.Make (struct let p = 13 end)
open B

Running it.

f = x^8 + x^6 - 3x^4 - 3x^3 - 5x^2 + 2x - 5, gcd(f, f') = 1
Q - I:
0 0 0 0 0 0 0 0
2 0 7 11 10 12 5 11
3 6 3 3 0 4 7 2
4 3 6 4 1 6 2 3
2 11 8 8 2 1 3 11
6 11 8 6 2 6 10 9
5 11 7 10 0 11 6 12
3 3 12 5 0 11 9 11
kernel basis (3 vectors):
1
x^6 + 5x^5 - 4x^4 + 5x^2 + 5x
x^7 - x^5 - 3x^4 - 4x^3 - 2x^2 - 4x
r = 3 factors:
x + 3 (its own r = 1)
x^3 - 5x^2 + 4x - 1 (its own r = 1)
x^4 + 2x^3 + 3x^2 + 4x + 6 (its own r = 1)
product equals f: true

This is the example Knuth works through in Seminumerical Algorithms[3]. The rank of is 5, so the null space has dimension 3 and the polynomial has three irreducible factors, of degrees 1, 3 and 4. Each factor on its own gives , which confirms its irreducibility.

Checking the count

Over all monic polynomials of degree 6 can be checked. For every square-free one, the dimension of the null space is compared with the number of irreducible factors found by trial division:

An exhaustive check over F_3.

(* For every monic polynomial of degree 6 over F_3: when it is square-free,
compare dim ker(Q - I) with the number of irreducible factors found by
trial division. *)
module B = Berl.Make (struct let p = 3 end)
open B
let monics d = (* all monic polynomials of degree d *)
List.init (int_of_float (3. ** float d)) (fun k ->
let c = Array.make (d + 1) 1 in
let k = ref k in
for i = 0 to d - 1 do c.(i) <- !k mod 3; k := !k / 3 done;
c)
let irreducibles = (* monic irreducibles of degree 1 to 3, by trial division *)
List.fold_left (fun acc d ->
acc @ List.filter (fun f -> List.for_all (fun g -> rem f g <> zero) acc) (monics d)) [] [ 1; 2; 3 ]
let count_factors f =
let rec go f = function
| [] -> if deg f > 0 then 1 else 0
| g :: gs -> if deg g > deg f then go f [] else if rem f g = zero then 1 + go (div f g) (g :: gs) else go f gs
in
go f irreducibles

Running it.

729 monic polynomials of degree 6 over F_3, 486 square-free
dim ker(Q - I) equals the number of irreducible factors for all of them: true
1 factor : 116 polynomials
2 factors: 226 polynomials
3 factors: 127 polynomials
4 factors: 17 polynomials

That the count needs only a rank makes the algorithm useful even when factors are found by other means. The Cantor–Zassenhaus algorithm shares none of its code, so comparing the number of factors it finds with is an independent check on an implementation.

§ 05

Large fields

Trying every costs gcds per split, which is impractical for large . Berlekamp's 1970 paper gave methods for large fields[2], and the randomized approach now standard takes a random and computes one gcd. For odd , each component of a random is uniform on , and is for about half of the nonzero values and for the other half:

which is a proper factor with probability at least about one half[4]. This is the same splitting step as in the Cantor–Zassenhaus algorithm, applied to an element of instead of a random polynomial of the right degree[5].

The randomized split over F_1000003.

(* For large p, trying every s in F_p is too slow. A random element v of
the Berlekamp subalgebra has v^((p-1)/2) equal to +1 or -1 in each
component, independently, so gcd(g, v^((p-1)/2) - 1) splits g with
probability about 1/2. *)
module B = Berl.Make (struct let p = 1_000_003 end)
open B
let () = Random.init 5
let split_random basis g =
let v = List.fold_left (fun acc b -> add acc (scale (Random.int p) b)) zero basis in
let w = sub (powmod (rem v g) ((p - 1) / 2) g) one in
gcd g w
let rec factor_rand basis f =
if List.length (kernel (q_matrix f)) = 1 then [ f ]
else
let rec try_ n = let h = split_random basis f in
if deg h > 0 && deg h < deg f then (h, n) else try_ (n + 1) in
let h, _ = try_ 1 in
factor_rand basis h @ factor_rand basis (div f h)

Running it.

f = x^10 + 7x^9 + 5x^8 + 41x^7 + 49x^6 + 64x^5 + 115x^4 + 81x^3 + 93x^2 + 122x + 70 over F_1000003, square-free: true
r = 5
x + 7
x + 410588
x - 410588
x^3 + 3x + 5
x^4 + x + 1
product equals f: true
§ 06

Complexity

Building takes the power , computed by repeated squaring in multiplications modulo , followed by multiplications by it. The null space costs field operations by Gaussian elimination. Deterministic splitting costs and randomized splitting an expected , so the randomized algorithm runs in time polynomial in and [4]. The cubic linear algebra dominates for large degrees. Kaltofen and Lobo replaced it with Wiedemann's black-box linear algebra, which only multiplies vectors by [8], and Kaltofen and Shoup gave algorithms subquadratic in the degree[9].

§ 07

Uses

Factoring polynomials over the integers reduces modulo a prime for which the polynomial remains square-free, factors there, and lifts the factors to a larger modulus by Hensel lifting, as proposed by Zassenhaus[7]; the lifted factors are then recombined by trial or, in modern systems, by lattice reduction. The Swinnerton-Dyer polynomials, which split into many factors modulo every prime, are the standard hard case for the recombination step. Finite-field factorization is also used in coding theory, where is factored to construct cyclic codes, and in cryptography.

§ 08

History

Petr studied the matrix of the Frobenius map in 1937[6]. Berlekamp, working on the decoding of error-correcting codes at Bell Labs, published the factorization algorithm in 1967[1] and methods for large fields in 1970[2]. Zassenhaus combined it with Hensel lifting to factor integer polynomials in 1969[7], Knuth presented it in Seminumerical Algorithms[3], and Cantor and Zassenhaus's 1981 algorithm replaced the linear algebra with exponentiation for most purposes[5].

see also

further reading

  1. [1]E. R. Berlekamp, “Factoring polynomials over finite fields”, Bell System Technical Journal 46 (1967).
  2. [2]E. R. Berlekamp, “Factoring polynomials over large finite fields”, Mathematics of Computation 24 (1970).
  3. [3]D. E. Knuth, The Art of Computer Programming, Vol. 2, §4.6.2, Addison-Wesley (3rd ed., 1997).
  4. [4]J. von zur Gathen, J. Gerhard, Modern Computer Algebra, ch. 14, Cambridge University Press (3rd ed., 2013).
  5. [5]D. G. Cantor, H. Zassenhaus, “A new algorithm for factoring polynomials over finite fields”, Mathematics of Computation 36 (1981).
  6. [6]K. Petr, “Über die Reduzibilität eines Polynoms mit ganzzahligen Koeffizienten nach einem Primzahlmodul”, Časopis pro pěstování matematiky a fysiky 66 (1937).
  7. [7]H. Zassenhaus, “On Hensel factorization, I”, Journal of Number Theory 1 (1969).
  8. [8]E. Kaltofen, A. Lobo, “Factoring high-degree polynomials by the black box Berlekamp algorithm”, ISSAC (1994).
  9. [9]E. Kaltofen, V. Shoup, “Subquadratic-time factoring of polynomials over finite fields”, Mathematics of Computation 67 (1998).
  10. [10]R. Lidl, H. Niederreiter, Finite Fields, Cambridge University Press (2nd ed., 1997).

last updated