Day 061-Q5 — 多項式多点評価・補間(積多項式ツリー)

2026-06-14 赤色 Master / Phase 8+ ★★★★★★★★★ 多項式多点評価 / Lagrange 補間 / 積多項式ツリー / NTT

問題

$N-1$ 次以下の多項式 $f(x)$ が係数列として与えられる。また $N$ 個の異なる点 $x_0, x_1, \ldots, x_{N-1}$ が与えられる。

Part 1 (多点評価): $f(x_0), f(x_1), \ldots, f(x_{N-1})$ を全て $\bmod p$ で求めよ。

Part 2 (多項式補間): $N$ 個の点 $(x_i, y_i)$ を通る $N-1$ 次以下の多項式 $g(x)$ の係数列を求めよ。

$p = 998244353$。

制約

パラメータ範囲
$N$$1 \le N \le 2^{17} = 131072$
係数・$x_i$・$y_i$$\in [0, p)$
$x_i$全て相異なる

入出力例

入力例 1

4
1 0 1 0
0 1 2 3
0 1 4 9

出力例 1

1 2 5 10
1 0 1 0

$f(x) = x^2 + 1$。評価: $f(0)=1, f(1)=2, f(2)=5, f(3)=10$。補間: $(0,0),(1,1),(2,4),(3,9)$ を通る多項式 → $g(x) = x^2$ → 係数 $[0,0,1,0]$。実際 $y = [0,1,4,9] = x^2$ なので $g$ の係数は $[0,0,1,0]$。

概念図: 積多項式ツリーと多点評価

積多項式ツリー(N=4, 点: x₀,x₁,x₂,x₃) (x-x₀)(x-x₁)(x-x₂)(x-x₃) M_root (degree 4) (x-x₀)(x-x₁) = M_L degree 2 (x-x₂)(x-x₃) = M_R degree 2 (x-x₀) (x-x₁) (x-x₂) (x-x₃) 多点評価 (Top-Down): f_root → f mod M_L, f mod M_R(degree が半分に) f mod M_L → f mod (x-x₀) = f(x₀), f mod (x-x₁) = f(x₁) 補間 (Bottom-Up): c_i を葉に置き、上に向かって c_i·兄弟積 を足し上げ

ヒント(段階的開示)

ヒント1: 方向性

朴素な多点評価は $O(N^2)$(各点で $f$ を評価)。$N = 131072$ には間に合わない。積多項式ツリー(Product Tree) を使い分割統治で $O(N \log^2 N)$ にする。

ヒント2: アプローチ
  • 積多項式ツリー構築: $M_i = (x - x_i)$ を葉とし、親 = 左子 × 右子。根は全点の積多項式(degree N)。$O(N \log^2 N)$。
  • 多点評価(Top-Down): f mod 左子の積f mod 右子の積 に再帰。葉では f mod (x-x_i) = f(x_i)
  • Lagrange 補間(Bottom-Up): $c_i = y_i / f'(x_i)$ を計算し、Bottom-Up で $\sum_i c_i \prod_{j\ne i}(x-x_j)$ を構築。
ヒント3: コード骨格
# 積多項式ツリーの構築
def build_ptree(pts, sz):
    tree = [[1]] * (2*sz)
    for i, x in enumerate(pts):
        tree[sz+i] = [(-x)%MOD, 1]  # (x - x_i)
    for i in range(sz-1, 0, -1):
        tree[i] = poly_mul(tree[2*i], tree[2*i+1])
    return tree

# 多点評価(Top-Down)
def multieval(f, tree, sz, n):
    vals = [None]*(2*sz)
    vals[1] = poly_mod(f, tree[1])
    for i in range(1, sz):
        vals[2*i]   = poly_mod(vals[i], tree[2*i])
        vals[2*i+1] = poly_mod(vals[i], tree[2*i+1])
    return [vals[sz+i][0] if vals[sz+i] else 0 for i in range(n)]

模範解答 (Python)

import sys
input = sys.stdin.readline
MOD = 998244353

def ntt(a, inv=False):
    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(3, (MOD-1)//length, MOD)
        if inv: 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 inv:
        inv_n = pow(n, MOD-2, MOD)
        for i in range(n): a[i]=a[i]*inv_n%MOD

def poly_mul(a, b):
    if not a or not b: return [0]
    n = 1
    while n < len(a)+len(b)-1: n <<= 1
    fa=a+[0]*(n-len(a)); fb=b+[0]*(n-len(b))
    ntt(fa); ntt(fb)
    fc=[x*y%MOD for x,y in zip(fa,fb)]
    ntt(fc, inv=True)
    return fc[:len(a)+len(b)-1]

def poly_inv(f, K):
    g = [pow(f[0], MOD-2, MOD)]; sz = 1
    while sz < K:
        sz2 = min(sz*2, K)
        ng = 1
        while ng < sz2*2: ng <<= 1
        fg = f[:sz2]+[0]*(ng-min(len(f),sz2))
        gg = g+[0]*(ng-len(g))
        ntt(fg); ntt(gg)
        tmp=[((2 if i==0 else 0)-fg[i]*gg[i])%MOD*gg[i]%MOD for i in range(ng)]
        ntt(tmp, inv=True); g=tmp[:sz2]; sz=sz2
    return g[:K]

def poly_divmod(a, b):
    if len(a) < len(b): return [0], a[:]
    n = len(a)-len(b)+1
    ar=a[::-1][:n]; br=b[::-1][:n]
    q_rev=poly_mul(ar, poly_inv(br, n))[:n]
    q=q_rev[::-1]
    r_full=poly_mul(q, b)
    r=[(a[i]-(r_full[i] if i

Step-by-Step 解説

Step 1: 積多項式ツリーの構築

セグメント木構造で、葉 $i$ に $(x - x_i)$ を置き、親 = 左子 × 右子。根は $\prod_i (x - x_i)$(degree N)。各レベルで NTT 乗算 $O(N \log N)$、$\log N$ レベルで合計 $O(N \log^2 N)$。

Step 2: 多点評価(Top-Down)

根の vals[1] = f mod M_root から出発し、各ノードで vals[2i] = vals[i] mod 左子積vals[2i+1] = vals[i] mod 右子積 と降下。葉では次数が 0 の定数多項式 = $f(x_i)$ が得られる。

Step 3: Lagrange 補間の前計算

$f'(x) = \frac{d}{dx}\prod_i(x-x_i)$ を計算し、多点評価で $f'(x_i) = \prod_{j \ne i}(x_i - x_j)$ を得る。$c_i = y_i / f'(x_i)$ が Lagrange 係数。

Step 4: 補間の Bottom-Up 計算

$c_i$ を葉に置き、vals[i] = vals[2i] × tree[2i+1] + vals[2i+1] × tree[2i] と Bottom-Up で合成。根の値が $g(x) = \sum_i c_i \prod_{j \ne i}(x - x_j)$。

よくあるミス

ミス原因正しい書き方
poly_mod で次数チェックを怠るlen(f) < len(m) のとき divmod が壊れるif len(f) < len(m): return f[:]
余分な葉(sz > n の部分)に [0] を置く乗算単位元は [1] のはず余分な葉は [1](定数多項式 1)で初期化
微分のインデックスがズレるderiv[i] = (i)*root[i] でなく (i+1)*root[i+1] と混同[(i*root[i])%MOD for i in range(1, len(root))]
Bottom-Up で左右の長さが異なる短い方の範囲外アクセスmax(len(l), len(r)) で長い方に合わせる

次のステップ

  • 発展問題: 同じ $x_i$ セットに複数の $f$ を評価(積多項式ツリーを再利用して $O(N \log^2 N)$ × バッチ数)
  • 関連: Chirp-Z Transform(等差数列点での多点評価を $O(N \log N)$ で)

自己評価