問題
$N \times N$ の整数行列 $A$ が与えられる。素数 $p$ に対して $\det(A) \bmod p$ を求めよ。
ここで $\det(A)$ は行列 $A$ の行列式(determinant)である。 通常の Gaussian elimination では各ステップで割り算が発生して分数が現れるが、 Bareiss アルゴリズムを使うことで整数演算のみで行列式を計算できる。 mod $p$ 上ではモジュラー逆元を使った Gaussian elimination が実装上シンプル。
入力の全要素は既に $0 \le A_{i,j} < p$ の範囲で与えられる。
入力形式
N p
A_{1,1} A_{1,2} ... A_{1,N}
A_{2,1} A_{2,2} ... A_{2,N}
...
A_{N,1} A_{N,2} ... A_{N,N}
制約
| パラメータ | 範囲 | 備考 |
|---|---|---|
| $N$ | $1 \le N \le 500$ | 行列のサイズ |
| $p$ | $2 \le p \le 10^9 + 7$(素数) | modulus(素数保証) |
| $A_{i,j}$ | $0 \le A_{i,j} < p$ | 入力は既に mod p 済み |
入出力例
入力例1(3×3)
3 1000000007
1 2 3
4 5 6
7 8 10
出力例1
1000000004
$\det(A) = -3$。$-3 \bmod (10^9+7) = 10^9+7-3 = 1000000004$。 展開: $1(50-48) - 2(40-42) + 3(32-35) = 2+4-9 = -3$。
入力例2(4×4)
4 998244353
2 1 3 4
0 3 1 2
5 2 4 1
1 0 2 3
出力例2
25
Bareiss アルゴリズムで計算すると $\det = 25$(整数値)。$25 \bmod 998244353 = 25$。
入力例3(特異行列)
3 7
1 2 3
4 5 6
7 8 9
出力例3
0
行3 = 行1 + 2×行2 なので $\det = 0$(特異行列)。ピボットが見つからない列が存在 → 0 を返す。
概念図: Gaussian Elimination のピボット操作
Gaussian elimination: 各列 $k$ でピボット(赤枠)を選び、下の行を消去して上三角行列を作る。 対角積に符号(行交換の回数)を掛けたものが行列式。mod $p$ 上では除算をモジュラー逆元に置き換える。
ヒント
ヒント1(方向性)
$\det(A)$ を素朴な Gaussian elimination で求めると、各ステップで割り算が発生して分数になる。
$p$ が素数ならモジュラー逆元 pow(x, p-2, p) で割り算を $O(\log p)$ で代替できるが、
ピボットが 0 の場合に逆元が存在しないため列(または行)交換が必要。
Bareiss アルゴリズムは「前のステップのピボットで割る」ことで分母を管理し、 整数行列なら整数演算だけで行列式を求める。mod $p$ 上では割り算をモジュラー逆元に変えて使える。
ヒント2(アプローチ)
方法A: Gaussian elimination mod p(推奨)
- 各列 $k$ でピボット行を見つける(
A[k][k] != 0を探して行交換) - 行交換のたびに
sign *= -1 - 行 $i > k$ に対して:
A[i] -= A[i][k] * modinv(A[k][k]) * A[k](mod p) - 対角積 $\prod_{k} A[k][k]$ に符号を掛けて答え
方法B: Bareiss アルゴリズム
漸化式: $$A^{(k)}_{i,j} = \frac{A^{(k-1)}_{k,k} \cdot A^{(k-1)}_{i,j} - A^{(k-1)}_{i,k} \cdot A^{(k-1)}_{k,j}}{A^{(k-2)}_{k-1,k-1}}$$ mod $p$ では分母をモジュラー逆元で処理。
ヒント3(コードスケルトン)
def modinv(a, p):
# Fermat の小定理: a^(p-1) ≡ 1 (mod p) → a^(-1) ≡ a^(p-2) (mod p)
return pow(a, p - 2, p)
def det_mod(A, n, p):
M = [row[:] for row in A] # コピー(元を破壊しない)
sign = 1
for k in range(n):
# ピボット選択: A[k][k] != 0 となる行を探す
pivot = -1
for i in range(k, n):
if M[i][k] != 0:
pivot = i
break
if pivot == -1:
return 0 # 特異行列: det = 0
if pivot != k:
M[k], M[pivot] = M[pivot], M[k]
sign = -sign # 行交換で符号反転
# 前進消去
inv_piv = modinv(M[k][k], p)
for i in range(k + 1, n):
factor = M[i][k] * inv_piv % p
for j in range(k, n):
M[i][j] = (M[i][j] - factor * M[k][j]) % p
# 対角積
result = sign % p
for k in range(n):
result = result * M[k][k] % p
return result % p
模範解答
方法A: Gaussian elimination mod p(推奨・シンプル)
import sys
def solve():
data = sys.stdin.buffer.read().split()
idx = 0
N, MOD = int(data[idx]), int(data[idx + 1])
idx += 2
A = []
for i in range(N):
row = [int(data[idx + j]) % MOD for j in range(N)]
A.append(row)
idx += N
print(det_mod_gaussian(A, N, MOD))
def modinv(a, m):
"""Fermat の小定理による逆元(m は素数であること)"""
return pow(a, m - 2, m)
def det_mod_gaussian(A, n, mod):
"""
Gaussian elimination mod p で行列式を計算。
時間計算量: O(N^3 * log(mod)) ← modinv 呼び出しが O(N^2) 回
ただし pow() の定数が小さいため実測は O(N^3) に近い
空間計算量: O(N^2)
"""
M = [row[:] for row in A] # 元の行列を破壊しないためコピー
sign = 1 # 行交換の符号
for k in range(n):
# ─── ピボット選択: 列 k で最初の非ゼロ要素を持つ行を探す ─
pivot_row = -1
for i in range(k, n):
if M[i][k] != 0:
pivot_row = i
break
if pivot_row == -1:
return 0 # 列 k が全て 0 → 特異行列 → det = 0
# 行交換(必要な場合のみ)
if pivot_row != k:
M[k], M[pivot_row] = M[pivot_row], M[k]
sign = -sign # 行交換で行列式の符号が反転
# ─── 前進消去: ピボット行より下の行を消去 ───────────────
inv_pivot = modinv(M[k][k], mod)
for i in range(k + 1, n):
if M[i][k] == 0:
continue # 既に 0 → スキップで定数倍高速化
factor = M[i][k] * inv_pivot % mod
for j in range(k, n):
M[i][j] = (M[i][j] - factor * M[k][j]) % mod
# ─── 上三角行列の対角積が行列式(符号も掛ける) ────────────
result = sign % mod # sign は -1 または 1
for k in range(n):
result = result * M[k][k] % mod
return result % mod # Python の % は常に非負
solve()
方法B: Bareiss アルゴリズム mod p(分数フリー Gaussian elimination)
import sys
def solve_bareiss():
data = sys.stdin.buffer.read().split()
idx = 0
N, MOD = int(data[idx]), int(data[idx + 1])
idx += 2
A = []
for i in range(N):
row = [int(data[idx + j]) % MOD for j in range(N)]
A.append(row)
idx += N
print(det_mod_bareiss(A, N, MOD))
def det_mod_bareiss(A, n, mod):
"""
Bareiss アルゴリズム(分数フリー Gaussian elimination)の mod p 版。
更新式:
M[i][j] = (M[k][k] * M[i][j] - M[i][k] * M[k][j]) / M_prev_pivot
整数版では分母が必ず割り切れる(Sylvester の恒等式による被除性)。
mod p 版では / をモジュラー逆元に変換。
時間計算量: O(N^3)
空間計算量: O(N^2)
"""
M = [row[:] for row in A]
sign = 1
prev_pivot = 1 # A^{(k-1)}_{k-1,k-1}(初期値 = 1 = 単位元)
for k in range(n):
# ─── ピボット選択 ────────────────────────────────────────
pivot_row = -1
for i in range(k, n):
if M[i][k] != 0:
pivot_row = i
break
if pivot_row == -1:
return 0
if pivot_row != k:
M[k], M[pivot_row] = M[pivot_row], M[k]
sign = -sign
cur_pivot = M[k][k]
# 前ステップのピボットの逆元(prev_pivot = 1 のときは不要)
inv_prev = pow(prev_pivot, mod - 2, mod) if prev_pivot != 1 else 1
# ─── Bareiss の更新 ──────────────────────────────────────
for i in range(k + 1, n):
for j in range(k + 1, n):
# (cur_pivot * M[i][j] - M[i][k] * M[k][j]) / prev_pivot
val = (cur_pivot * M[i][j] - M[i][k] * M[k][j]) % mod
M[i][j] = val * inv_prev % mod
M[i][k] = 0 # 消去済みに設定(省略可能だが明示)
prev_pivot = cur_pivot
result = sign % mod
for k in range(n):
result = result * M[k][k] % mod
return result % mod
solve_bareiss()
Hessenberg 変換を使ったアプローチ(参考)
def to_hessenberg_det(A, n, mod):
"""
相似変換で上 Hessenberg 形に変換後、O(N^2) で行列式を計算。
Hessenberg 形: H[i][j] = 0 for i > j + 1
行列式は漸化式で計算: D[k] = H[k-1][k-1]*D[k-1] - H[k-1][k-2]*H[k-2][k-1]*D[k-2] + ...
"""
M = [row[:] for row in A]
# 列 k で k+2 行目以下を消去(相似変換: 行操作 + 対応する列操作)
for k in range(n - 2):
# サブ対角要素 M[k+1][k] を非ゼロにする
pivot = -1
for i in range(k + 1, n):
if M[i][k] != 0:
pivot = i
break
if pivot == -1:
continue
if pivot != k + 1:
# 行交換
M[k + 1], M[pivot] = M[pivot], M[k + 1]
# 対応する列交換(相似変換: det 不変)
for i in range(n):
M[i][k + 1], M[i][pivot] = M[i][pivot], M[i][k + 1]
inv_sub = pow(M[k + 1][k], mod - 2, mod)
for i in range(k + 2, n):
if M[i][k] == 0:
continue
factor = M[i][k] * inv_sub % mod
# 行 i から factor * 行(k+1) を引く
for j in range(n):
M[i][j] = (M[i][j] - factor * M[k + 1][j]) % mod
# 列 (k+1) に factor * 列 i を足す(行列式不変)
for j in range(n):
M[j][k + 1] = (M[j][k + 1] + factor * M[j][i]) % mod
# 上 Hessenberg 行列の行列式(漸化式)
D = [0] * (n + 1)
D[0] = 1
for k in range(1, n + 1):
D[k] = M[k - 1][k - 1] * D[k - 1] % mod
sub_diag_prod = 1
for i in range(k - 2, -1, -1):
sub_diag_prod = sub_diag_prod * M[i + 1][i] % mod
D[k] = (D[k] - M[k - 1][i] * sub_diag_prod % mod * D[i]) % mod
return D[n] % mod
Step-by-Step 解説
Step 1: 行列式の基本性質と Gaussian elimination の根拠
行列式には以下の基本変換が成り立つ(多重線形性・交代性):
| 操作 | 行列式への影響 | 使用場面 |
|---|---|---|
| 行 $i$ を $c$ 倍 | $\det$ も $c$ 倍 | 対角正規化 |
| 行 $i$ に $c \times$ 行 $j$ を加算($i \ne j$) | $\det$ は変わらない | 前進消去の核心 |
| 行 $i$ と行 $j$ を交換 | $\det$ の符号が反転 | ピボット選択 |
Gaussian elimination は性質2と3を使って上三角行列を作り、対角積を求める。
Step 2: mod p での除算とモジュラー逆元
mod $p$ 上での除算($a/b$)は Fermat の小定理で実装する:
$$b^{-1} \equiv b^{p-2} \pmod{p}$$
これが成り立つのは $p$ が素数のとき($b \not\equiv 0$)。Python では pow(b, p-2, p) が最速(内部で繰り返し二乗法)。
注意: ピボットが $0 \pmod{p}$ のとき逆元が存在しない。行交換で非ゼロ要素を持つ行を持ってくる必要がある。全ての行で 0 なら特異行列 → $\det = 0$。
Step 3: Bareiss アルゴリズムの仕組み
通常の Gaussian では各ステップで逆元計算($O(\log p)$)が $O(N^2)$ 回発生する。Bareiss の改善点: 各ステップで「前のピボット $A^{(k-1)}_{k-1,k-1}$」で割ることで、整数行列なら整数のまま計算を進められる(Sylvester の恒等式による被除性の保証)。
更新式:
$$A^{(k)}_{i,j} = \frac{A^{(k-1)}_{k,k} \cdot A^{(k-1)}_{i,j} - A^{(k-1)}_{i,k} \cdot A^{(k-1)}_{k,j}}{A^{(k-1)}_{k-1,k-1}}$$
- 整数版: 分母が必ず割り切れる → 純粋整数演算(多倍長整数注意)
- mod p 版: 分母をモジュラー逆元で処理(ただし逆元計算コストは同じ)
Step 4: Hessenberg 変換の概念
上 Hessenberg 形とは $H[i][j] = 0$ for $i > j+1$ の行列(対角の 1 つ下のサブ対角には非ゼロが残る):
上 Hessenberg 形:
* * * *
* * * *
0 * * *
0 0 * *
変換方法: 相似変換 $H = P^{-1}AP$ を使うことで $\det(H) = \det(A)$ を保ちつつ、列 $k$ の $k+2$ 行目以下を消去。消去するときに対応する列操作を同時に施す(相似変換なので $\det$ は不変)。Hessenberg 行列の行列式は展開式の漸化式で $O(N^2)$ だが、実装が複雑なため競技プログラミングでは方法A が主流。
Step 5: 計算量まとめ
| 手法 | 時間計算量 | 空間計算量 | 実装難易度 |
|---|---|---|---|
| Gaussian elimination mod p | $O(N^3 \log p)$ | $O(N^2)$ | 容易 |
| Bareiss mod p | $O(N^3 \log p)$ | $O(N^2)$ | やや難 |
| Hessenberg + 行列式 | $O(N^3)$ 変換 + $O(N^2)$ | $O(N^2)$ | 難 |
$N=500$ で $N^3 = 1.25\times10^8$。modinv は pow で高速化済み。実際は $O(N^3)$ 程度で動作。PyPy 推奨。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
modinv(0, p) を呼んでしまう |
ピボットが 0 のときに逆元を求めようとする | ピボット選択で A[k][k] != 0 を確認してから modinv を呼ぶ |
| 行交換を忘れる | A[k][k] = 0 でそのまま進むと 0 除算エラー |
必ずピボット選択ループで非ゼロ行を探す |
符号 sign を mod しない |
sign = -1 のまま最終計算で負になる |
result * sign % mod で Python の % が常に非負を保証 |
Bareiss で prev_pivot = 0 |
特異行列のケースで 0 除算 | Bareiss も行交換 + ピボットチェックが必要 |
| Hessenberg の列操作を忘れる | 行だけ消去すると行列式が変わる | 行消去のたびに対応する列操作を必ず施す(相似変換) |
| $N=1$ の特殊ケース | ループが 0 回でも result = A[0][0] で正しい |
特別処理不要。N=1 テストは確認する |
次のステップ
- 発展問題1: 行列式ではなく永久式(Permanent)を求めよ。行列式と異なり $O(N!)$ か Ryser の公式で $O(2^N N)$ 必要。
- 発展問題2: $\mathbb{F}_p[x]$ 上の多項式行列式(各要素が $x$ の多項式)を求めよ。結果は $x$ の多項式になり、係数ごとに mod p 計算が必要。
- 発展問題3: Sylvester 行列の行列式(= 2つの多項式の終結式 resultant)を $O(N^3)$ で計算し、多項式 GCD の次数を求めよ。
- AtCoder 関連問題: 行列木定理(Kirchhoff のマトリクス木定理)、行列累乗の行列式(det の $K$ 乗 = det(A^K))。