Day 081-Q4 — 多次元 NTT(テンソル積畳み込み・2D 多項式積)

2026-07-04 赤色 Master / Phase 8+ ★★★★★★★★★ Multidimensional NTT・テンソル積・2D-NTT・998244353

問題

2 次元多項式 $F(x, y) = \sum_{i=0}^{N-1}\sum_{j=0}^{M-1} a_{i,j} x^i y^j$ と $G(x, y)$ の積 $H = F \cdot G$ を求めよ。

$H(x, y) = \sum_{k=0}^{2N-2}\sum_{l=0}^{2M-2} c_{k,l} x^k y^l$ の全係数 $c_{k,l}$ を $998244353$ で割った余りで出力せよ。

制約

パラメータ範囲備考
$N, M$$1 \le N, M \le 500$係数行列サイズ
$a_{i,j}, b_{i,j}$$0 \le a_{i,j}, b_{i,j} < 998244353$係数
出力$(2N-1) \times (2M-1)$ の行列行列形式で出力

入出力例

入力例1

2 2
1 2
3 4
5 6
7 8

出力例1

5 16 12
22 60 40
21 52 32

概念図: 2D-NTT の処理フロー

多次元 NTT — テンソル積 $\text{NTT}_H \otimes \text{NTT}_W$ F (N×M) ゼロパディング → RH×RW (RH, RW: 2の冪) 行方向 NTT 各行に 1D-NTT 適用 O(RH·RW log RW) 列方向 NTT 各列に 1D-NTT 適用 O(RH·RW log RH) F̂ (NTT空間) 2D-NTT 完了 同様に Ĝ 要素積: Ĥ[i][j] = F̂[i][j] × Ĝ[i][j] mod p 逆 2D-NTT (列方向逆 NTT → 行方向逆 NTT) 各操作で $n^{-1}$ を掛ける (行: $RW^{-1}$, 列: $RH^{-1}$) 計算量: 行方向: $O(RH \cdot RW \log RW)$, 列方向: $O(RH \cdot RW \log RH)$ 全体: $O(NM(\log N + \log M)) = O(NM \log(NM))$ テンソル積 $\text{NTT}_H \otimes \text{NTT}_W$ は可換 → 行先・列先どちらでも同じ結果

ヒント

ヒント1(方向性)

2次元多項式の積は,1次元 NTT を 2 方向(行・列)に独立に適用することで計算できる。「行に NTT → 列に NTT → 要素積 → 逆列 NTT → 逆行 NTT」の 5 ステップ。

ヒント2(アプローチ)

ゼロパディングのサイズ:畳み込み結果が $(2N-1) \times (2M-1)$ なので,$\ge 2N-1$ の最小 2 の冪 $RH$ と $\ge 2M-1$ の最小 2 の冪 $RW$ にパディング。

ヒント3(ほぼ答え)
def ntt2d(A, H, W, invert):
    # 行方向 NTT
    for i in range(H): ntt(A[i], invert)
    # 列方向 NTT
    for j in range(W):
        col = [A[i][j] for i in range(H)]
        ntt(col, invert)
        for i in range(H): A[i][j] = col[i]

模範解答

import sys
input = sys.stdin.readline

MOD = 998244353
g_prim = 3

def ntt(a, invert):
    n = len(a)
    j = 0
    for i in range(1, n):
        bit = n >> 1
        while j & bit: j ^= bit; bit >>= 1
        j ^= bit
        if i < j: a[i], a[j] = a[j], a[i]
    length = 2
    while length <= n:
        w = pow(g_prim, (MOD-1)//length, MOD)
        if invert: w = pow(w, MOD-2, MOD)
        for i in range(0, n, length):
            wn = 1
            for k in range(length//2):
                u = a[i+k]; v = a[i+k+length//2] * wn % MOD
                a[i+k] = (u+v) % MOD; a[i+k+length//2] = (u-v) % MOD
                wn = wn*w % MOD
        length <<= 1
    if invert:
        inv_n = pow(n, MOD-2, MOD)
        for i in range(n): a[i] = a[i]*inv_n % MOD

def ntt2d(A, H, W, invert):
    for i in range(H): ntt(A[i], invert)
    for j in range(W):
        col = [A[i][j] for i in range(H)]
        ntt(col, invert)
        for i in range(H): A[i][j] = col[i]

def solve():
    N, M = map(int, input().split())
    A = [list(map(int, input().split())) for _ in range(N)]
    B = [list(map(int, input().split())) for _ in range(N)]

    RH = 1
    while RH < 2*N-1: RH <<= 1
    RW = 1
    while RW < 2*M-1: RW <<= 1

    FA = [[0]*RW for _ in range(RH)]
    FB = [[0]*RW for _ in range(RH)]
    for i in range(N):
        for j in range(M):
            FA[i][j] = A[i][j]; FB[i][j] = B[i][j]

    ntt2d(FA, RH, RW, False)
    ntt2d(FB, RH, RW, False)

    FC = [[FA[i][j]*FB[i][j]%MOD for j in range(RW)] for i in range(RH)]
    ntt2d(FC, RH, RW, True)

    out = []
    for i in range(2*N-1):
        out.append(' '.join(str(FC[i][j]) for j in range(2*M-1)))
    print('\n'.join(out))

solve()

Step-by-Step 解説

Step 1: 2次元多項式積の基本原理

$H[k][l] = \sum_{i+j=k}\sum_{p+q=l} F[i][p] \cdot G[j][q]$ — 「行方向の畳み込み」と「列方向の畳み込み」を独立に適用することと等価。

Step 2: テンソル積としての理解

2D-NTT は $\text{NTT}_H \otimes \text{NTT}_W$(Kronecker 積)。行→列の順でも列→行の順でも同じ変換結果になる(可換性)。

Step 3: ゼロパディングサイズの決定

畳み込み結果のサイズ $(2N-1) \times (2M-1)$ なので,$\ge 2N-1$ の最小 2 の冪 $RH$,$\ge 2M-1$ の最小 2 の冪 $RW$ が必要。

Step 4: 計算量の確認

行方向 NTT: $O(RH \cdot RW \log RW)$,列方向 NTT: $O(RH \cdot RW \log RH)$,全体: $O(NM \log(NM))$。逆 NTT も同様。

よくあるミス

ミス原因正しい書き方
パディングサイズ不足$2N-1$ 以上の 2 の冪が必要while RH < 2*N-1: RH <<= 1
列 NTT で転置を忘れる列ごとにアクセスが必要col = [A[i][j] for i in range(H)]
逆 NTT の $n^{-1}$ が 2 回かかる行と列それぞれに $n^{-1}$ がかかる合計で $(RH \cdot RW)^{-1}$ — 正しい挙動

次のステップ

  • 発展問題: 3次元 NTT($N \times M \times L$ 多項式の積)
  • 多変数多項式剰余(mod $p(x), q(y)$)の高速計算

自己評価

理解度: / /

自分の回答:

気づき・メモ: