Day 124-Q5 — Berlekamp's Q-Matrix Algorithm(有限体上の多項式因数分解)

2026-08-16 赤色 Master / Phase 8+ ★★★★★★★★★ Berlekampのアルゴリズム(Q行列の核空間 + 多項式GCDによる有限体上の重複因子なし多項式の完全因数分解)

問題

素数 $p$ と、有限体 $\mathrm{GF}(p)$ 上の $n$ 次多項式 $f(x)$(重複因子を持たないことが保証される、すなわち $f$ とその形式微分 $f'$ が互いに素)が与えられる。$f(x)$ を $\mathrm{GF}(p)$ 上で既約な多項式の積に因数分解し、既約因子をすべて出力せよ(順序は次数昇順、同次数内は係数の辞書順で構わない)。

入力形式

p n
c_0 c_1 ... c_n

制約

$2 \le p \le 97$(素数)
$1 \le n \le 30$
$f$ は $\mathrm{GF}(p)$ 上で重複因子を持たない($\gcd(f, f') = 1$)
$0 \le c_i < p$、$c_n \ne 0$

入出力例

入力例1

5 4
1 0 0 0 1

出力例1

2
2 2 0 1
2 3 0 1

$f(x) = x^4+1$ を $\mathrm{GF}(5)$ 上で因数分解すると $(x^2+2)(x^2+3)$ となる(実際 $(x^2+2)(x^2+3)=x^4+5x^2+6 \equiv x^4+1 \pmod 5$)。1行目は因子の個数、以降各行は「次数 + 係数(低次から高次)」

概念図

Berlekampのアルゴリズム: 因数分解のパイプライン f(x) 次数n square-free Q行列構築 行i = x^(ip) mod f Q-I の核空間 次元 k = 既約因子数 k=1? 既約確定 k≥2 の場合 gcd(f, v-s) を s=0..p-1 全部試す 得られたgcd群は互いに素で積がfに戻る → 1回の基底ベクトルで最大限分解 因子の個数が k に達するまで繰り返し → 既約分解完了

ヒント(段階的開示)

ヒント1(方向性)

有限体上では「$x^p \equiv x \pmod{p}$」というフェルマーの小定理が成り立つが、これを多項式に拡張すると、$\mathrm{GF}(p)$ 上で $f$ が既約因子 $f = f_1 f_2 \cdots f_k$ に分解するとき、剰余環 $\mathrm{GF}(p)[x]/(f)$ の中で「Frobenius写像 $\phi: g \mapsto g^p \bmod f$」が中国剰余定理を通じて各因子ごとに独立に振る舞うという性質が使える。この線形性を利用して、因数分解を線形代数の問題(行列の核空間探索)に帰着できるのがBerlekampのアルゴリズムの核心である。

ヒント2(アプローチ)

$n \times n$ 行列 $Q$ を、$Q$ の第 $i$ 行が「$x^{ip} \bmod f(x)$ の係数ベクトル」となるように構成する。$Q - I$($I$は単位行列)の核空間(null space) を $\mathrm{GF}(p)$ 上のガウス消去法で求めると、その次元がちょうど既約因子の個数 $k$ に一致する(これは「Frobenius写像の固定点全体」=「各既約因子への射影で定数になる元全体」という中国剰余定理の帰結)。$k=1$ なら $f$ はすでに既約。$k \ge 2$ なら、核空間の基底ベクトル(多項式とみなす)$v$ と定数 $s \in \mathrm{GF}(p)$ の組について $\gcd(f, v - s)$ を計算すると、$f$ の真の約数が得られる。

ヒント3(誘導)

分解のメインループ:

factors = [f]
for basis_vec in null_space_basis:  # 定数ベクトル以外
    if len(factors) == k:
        break
    new_factors = []
    for g in factors:
        if degree(g) == 1:
            new_factors.append(g)
            continue
        # 1つの s で打ち切らず、s=0..p-1 全部を元の g に対して試す。
        # 見つかった gcd 達は互いに素で、掛け合わせると g に戻る性質を利用し、
        # 1回の基底ベクトルで可能な限り深く分解する。
        pieces, deg_sum = [], 0
        for s in range(p):
            h = poly_gcd(g, poly_sub(basis_vec, [s], p), p)
            if degree(h) > 0:
                pieces.append(h)
                deg_sum += degree(h)
                if deg_sum == degree(g):
                    break
        new_factors.extend(pieces if len(pieces) >= 2 else [g])
    factors = new_factors

模範解答 (Python)

import sys

def solve():
    data = sys.stdin.read().split()
    idx = 0
    p = int(data[idx]); idx += 1
    n = int(data[idx]); idx += 1
    f = [int(data[idx + i]) for i in range(n + 1)]
    idx += n + 1

    def trim(a):
        a = a[:]
        while len(a) > 1 and a[-1] == 0:
            a.pop()
        return a

    def padd(a, b):
        m = max(len(a), len(b))
        r = [0] * m
        for i, x in enumerate(a): r[i] = (r[i] + x) % p
        for i, x in enumerate(b): r[i] = (r[i] + x) % p
        return trim(r)

    def psub(a, b):
        m = max(len(a), len(b))
        r = [0] * m
        for i, x in enumerate(a): r[i] = (r[i] + x) % p
        for i, x in enumerate(b): r[i] = (r[i] - x) % p
        return trim(r)

    def pmul(a, b):
        r = [0] * (len(a) + len(b) - 1)
        for i, ai in enumerate(a):
            if ai == 0: continue
            for j, bj in enumerate(b):
                r[i + j] = (r[i + j] + ai * bj) % p
        return trim(r)

    def pdivmod(a, b):
        a = trim(a); b = trim(b)
        if len(a) < len(b):
            return [0], a
        q = [0] * (len(a) - len(b) + 1)
        r = a[:]
        inv_lead = pow(b[-1], p - 2, p)
        while len(trim(r)) >= len(b) and any(r):
            r = trim(r)
            if len(r) < len(b):
                break
            coef = (r[-1] * inv_lead) % p
            shift = len(r) - len(b)
            q[shift] = coef
            for i, bi in enumerate(b):
                r[shift + i] = (r[shift + i] - coef * bi) % p
            r = trim(r)
        return trim(q), trim(r)

    def pmod(a, mod):
        return pdivmod(a, mod)[1]

    def pgcd(a, b):
        a, b = trim(a), trim(b)
        while not (len(b) == 1 and b[0] == 0):
            _, r = pdivmod(a, b)
            a, b = b, (r if r else [0])
        inv = pow(a[-1], p - 2, p)
        return trim([c * inv % p for c in a])

    def degree(a):
        a = trim(a)
        return len(a) - 1 if not (len(a) == 1 and a[0] == 0) else -1

    # --- x^p mod f を高速べき乗で求める ---
    def ppow_mod(e, mod):
        result = [1]
        base = [0, 1]
        while e:
            if e & 1:
                result = pmod(pmul(result, base), mod)
            base = pmod(pmul(base, base), mod)
            e >>= 1
        return result

    xp = ppow_mod(p, f)

    # --- Q行列構築: 行i = x^(i*p) mod f の係数(長さnにパディング) ---
    Q = [[0] * n for _ in range(n)]
    cur = [1]
    for i in range(n):
        for j, c in enumerate(cur):
            if j < n:
                Q[i][j] = c
        cur = pmod(pmul(cur, xp), f)

    # Q - I
    for i in range(n):
        Q[i][i] = (Q[i][i] - 1) % p

    # --- ガウス消去で Q-I の核空間を求める ---
    mat = [row[:] for row in Q]
    pivot_cols = []
    row_i = 0
    for col in range(n):
        piv = -1
        for r in range(row_i, n):
            if mat[r][col] != 0:
                piv = r
                break
        if piv == -1:
            continue
        mat[row_i], mat[piv] = mat[piv], mat[row_i]
        inv = pow(mat[row_i][col], p - 2, p)
        mat[row_i] = [(x * inv) % p for x in mat[row_i]]
        for r in range(n):
            if r != row_i and mat[r][col] != 0:
                factor = mat[r][col]
                mat[r] = [(mat[r][c2] - factor * mat[row_i][c2]) % p for c2 in range(n)]
        pivot_cols.append(col)
        row_i += 1

    free_cols = [c for c in range(n) if c not in pivot_cols]
    k = len(free_cols)  # 既約因子の個数

    basis = []
    for fc in free_cols:
        vec = [0] * n
        vec[fc] = 1
        r = 0
        for pc in pivot_cols:
            vec[pc] = (-mat[r][fc]) % p
            r += 1
        basis.append(trim(vec))

    factors = [trim(f)]
    if k > 1:
        for bv in basis:
            if degree(bv) <= 0:
                continue  # 定数ベクトルは分解に使えない
            if len(factors) >= k:
                break
            new_factors = []
            for g in factors:
                if degree(g) == 1:
                    new_factors.append(g)
                    continue
                # s=0..p-1 それぞれについて gcd(g, v-s) を元の g に対して計算する。
                # v は各既約因子上で異なる定数値を取るため、こうして集めた gcd 達は
                # 互いに素であり、掛け合わせると g に戻る(中国剰余定理の帰結)。
                # 1つの s で打ち切らず全 s を試すことで、1回の基底ベクトルで
                # 最大限分解できる。
                pieces = []
                deg_sum = 0
                for s in range(p):
                    h = pgcd(g, psub(bv, [s]))
                    if degree(h) > 0:
                        pieces.append(h)
                        deg_sum += degree(h)
                        if deg_sum == degree(g):
                            break
                if len(pieces) >= 2:
                    new_factors.extend(pieces)
                else:
                    new_factors.append(g)
            factors = new_factors

    factors = [trim(fa) for fa in factors]
    factors.sort(key=lambda a: (degree(a), a))

    print(len(factors))
    for fa in factors:
        print(degree(fa), *fa)

solve()

Step-by-Step 解説

1Frobenius写像と中国剰余定理
$f = f_1 f_2 \cdots f_k$(既約因子への分解)のとき、中国剰余定理により $\mathrm{GF}(p)[x]/(f) \cong \prod_i \mathrm{GF}(p)[x]/(f_i)$ という同型が成り立つ。各成分 $\mathrm{GF}(p)[x]/(f_i)$ は体($f_i$ が既約なので)であり、体の中ではフェルマーの小定理から $g^p = g$ が成り立つ元、つまり $\mathrm{GF}(p)$ の元(定数)だけが「$\phi(g)=g$」を満たす。
2Q行列と核空間の意味
$Q$ は「Frobenius写像 $g \mapsto g^p \bmod f$」を係数ベクトルに対する線形写像として表現した行列である。$Q - I$ の核空間($Qv = v$ を満たす $v$)は、中国剰余分解のもとで「各成分ごとに定数になっている元全体」に対応し、その次元はちょうど既約因子の個数 $k$ に一致する(Berlekampの定理)。
3$k=1$ なら既約
核空間の次元が1(定数ベクトルのみ)であれば、$\mathrm{GF}(p)[x]/(f)$ 自体が体、つまり $f$ はすでに既約であることを意味する。
4核空間ベクトルによる分解
$k \ge 2$ のとき、非定数の核空間ベクトル $v$ を1つ選ぶと、$v$ は中国剰余分解の各成分で異なる定数値 $s_i \in \mathrm{GF}(p)$ を取る(少なくとも2つの成分で異なる値を取ることが、$v$ が非定数であることから保証される)。このとき $\gcd(f, v - s)$ は「$v \equiv s$ となる成分に対応する因子の積」になるので、適切な $s$ を全探索すれば $f$ を真に分割できる。
5分割の繰り返し
得られた真の約数それぞれに対して、別の基底ベクトルや別の $s$ で同様の分割を試みることを、因子の個数が $k$ に達するまで繰り返す。すべての因子が次数1になるか、核空間の次元と一致した時点で完全な既約分解が得られる。

よくあるミス

ミス原因正しい書き方
$Q$ 行列を $x^p, x^{2p}, \ldots$ を毎回ゼロから高速べき乗で計算し $O(n^2 \log p)$ 回余分にかける増分計算で十分なことに気づいていない$x^p \bmod f$ を1回だけ計算し、以降は前の行に掛けるだけで次の行が求まる
ガウス消去の割り算で逆元を使わず整数除算してしまう$\mathrm{GF}(p)$ 上の除算は逆元(フェルマーの小定理 $a^{p-2}$)を使う必要があることを忘れているpow(x, p-2, p) でモジュラ逆元を求めてから掛ける
重複因子がある入力を想定してしまう問題の前提「$\gcd(f,f')=1$」を見落としている重複因子がある場合は事前に $f / \gcd(f,f')$ で square-free化する前処理が別途必要(今回は保証されているので不要)
$\gcd(f, v-s)$ が $f$ 自身や定数になるケースを排除していない分割条件のチェック漏れ$0 < \deg(h) < \deg(g)$ を必ず確認してから分割を採用する
1つの基底ベクトルにつき、最初に見つかった $s$ で分割を打ち切ってしまう1つの $v$ が3つ以上の既約因子を同時に区別できる場合があることを見落としている(例: $(x-1)(x-2)(x-3)$ を $v=x$ で分割すると $s=1,2,3$ の3通りすべてが有効な約数を与える)$s=0,\ldots,p-1$ を全部試し、$\deg$ の合計が $\deg(g)$ に達するまで gcd を集め続ける

次のステップ

  • 発展: $p$ が大きい場合($p > n$ 程度)は $s$ の全探索が非効率になるため、Cantor-Zassenhaus法(ランダム多項式による確率的分割)を組み合わせるのが実用的。重複因子がある一般の入力には square-free分解($f/\gcd(f,f')$ の繰り返し)を前処理として組み合わせる。
  • 次回予告: Master Levelローテーション継続(次サイクルの5テーマ)

自己評価

自分の回答

気づき・メモ