Day 044-Q5 — 行列のパーマネント(Ryser法)

2026-05-28 赤色 Master / Phase 8+ ★★★★★★★★★ 包除原理 + Gray code

問題

$N \times N$ の0/1行列 $A$ について、パーマネント

$$ \mathrm{perm}(A) = \sum_{\sigma \in S_N} \prod_{i=1}^{N} A_{i,\sigma(i)} $$

を $\bmod\ 10^9+7$ で求めよ。これは行 $i$・列 $j$ を $A_{ij}=1$ で結ぶ二部グラフの完全マッチング総数に等しい。

制約

$1 \le N \le 20$
$A_{ij} \in \{0, 1\}$
$\bmod\ 10^9+7$
時間制限: 2sec

入出力例

入力例 1

3
1 1 0
0 1 1
1 0 1

出力例 1

2

概念図: Ryser の公式(列集合の包除)

列集合 S を全列挙し、各行の「S内の1の個数」の積を符号付き合計 perm(A) = (-1)^N · Σ_{S⊆cols} (-1)^|S| · Π_i ( Σ_{j∈S} A_ij ) Gray code で S を1列ずつ変化 {c0} {c0,c1} {c1} {c1,c2} +c1 -c0 +c2 差分更新 row_sum[i] を ±1 だけ更新 符号 sign を反転 → 各 S の積計算が主コスト perm は #P困難(Valiant)。行列式のガウス消去は使えない。Ryser で O(2^N · N)。

ヒント(段階的開示)

ヒント1: 方向性
定義通りは $N!$ で $N=20$ は不可能。行列式と違いガウス消去(多項式時間)は使えない(perm計算は #P困難)。Ryser の公式で $O(2^N N)$ に落とせる。
ヒント2: アプローチ
$$ \mathrm{perm}(A) = (-1)^N \sum_{S \subseteq \{1,\dots,N\}} (-1)^{|S|} \prod_{i} \Big( \sum_{j \in S} A_{ij} \Big) $$ 列の部分集合 $S$ を全列挙し、各行の「$S$ 内の列の和」を掛け合わせる。符号は $|S|$ の偶奇。
ヒント3: 誘導
Gray code で $S$ を1要素ずつ変化させ、各行の「$S$ 内の列和」を差分更新すれば定数時間で維持できる。素朴でも $O(2^N N) \approx 2\times10^7$ で間に合う。空集合は積0なので寄与なし。

模範解答 (Python)

import sys

def solve():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N = int(data[idx]); idx += 1
    MOD = 10**9 + 7
    rows = []
    for i in range(N):
        mask = 0
        for j in range(N):
            if int(data[idx]) == 1:
                mask |= (1 << j)
            idx += 1
        rows.append(mask)

    row_sum = [0]*N
    result = 0
    sign = 1
    prev = 0
    full = 1 << N
    for g in range(1, full):
        gray = g ^ (g >> 1)
        diff = gray ^ prev
        bit = diff.bit_length() - 1
        col = 1 << bit
        if gray & diff:   # 列 bit を追加
            for i in range(N):
                if rows[i] & col:
                    row_sum[i] += 1
            sign = -sign
        else:             # 列 bit を除去
            for i in range(N):
                if rows[i] & col:
                    row_sum[i] -= 1
            sign = -sign
        prev = gray
        prod = 1
        for i in range(N):
            prod = (prod * row_sum[i]) % MOD
            if prod == 0:
                break
        if prod:
            result = (result + sign * prod) % MOD

    if N & 1:
        result = (-result) % MOD
    print(result % MOD)

solve()

Step-by-Step 解説

1perm と #P困難性
行列式は符号付きで打ち消しがあり多項式時間だが、perm は符号がなく Valiant により #P困難。多項式時間アルゴリズムは未知。
2Ryser の公式
包除原理で $\mathrm{perm}(A)=(-1)^N\sum_S(-1)^{|S|}\prod_i(\sum_{j\in S}A_{ij})$。列集合の全列挙で $O(2^N N)$。
3行集合和の意味
$\sum_{j\in S}A_{ij}$ は行 $i$ が $S$ 内に持つ1の個数。全行の積は割当の場合の数で、包除で重複を打ち消すと順列割当(完全マッチング)が残る。
4Gray code 高速化
$S$ を Gray code 順に列挙すると隣接 $S$ は1列だけ変化。row_sum を $O(N)$ で差分更新でき、主コストは積計算 $O(2^N N)$。
5符号と mod
$(-1)^{|S|}$ を sign で管理、最後に $(-1)^N$ を掛ける。負値は % MOD で正規化。

計算量

Ryser (Gray code): $O(2^N N)$
$N=20$: $2^{20}\cdot 20 \approx 2\times 10^7$
空間: $O(N)$
定義通り $O(N!)$ は不可能

よくあるミス

ミス原因正しい書き方
定義通り $N!$ 計算全順列DFSRyser $O(2^N N)$
行列式のガウス消去を流用permは消去不可包除原理を使う
符号 $(-1)^N$ 忘れ公式の前因子最後にNの偶奇で調整
mod で負値放置sign*prod が負(x%MOD+MOD)%MOD
空集合を含める積0だが符号が乱れる$S=\emptyset$ はスキップ

次のステップ

  • 発展問題: 非0/1行列のperm、bitDPによる完全マッチング数え上げ $O(2^N N)$
  • 類題: 二部グラフ完全マッチング数、割当数え上げ
  • 応用: ボソンサンプリング(量子計算)、統計力学の分配関数

自己評価