問題
次数 $N-1$ の多項式 $f(x) = \sum_{i=0}^{N-1} c_i x^i$ と $M$ 個の評価点 $x_1, \ldots, x_M$ が与えられる。$f(x_1), \ldots, f(x_M)$ をすべて $p = 998244353$ で割った余りを求めよ。
ナイーブな $O(NM)$ ではなく、Subproduct Tree(多項式剰余木)を使い $O((N+M)\log^2(N+M))$ で解け。
制約
| パラメータ | 範囲 |
|---|---|
| $N, M$ | $1 \le N, M \le 2 \times 10^5$ |
| $c_i$ | $0 \le c_i < p$ |
| $x_j$ | $0 \le x_j < p$ |
| $p$ | $998244353$(NTT 対応素数) |
入出力例
入力例 1
4 3
1 2 3 4
0 1 2
出力例 1
1
10
49
$f(x) = 4x^3+3x^2+2x+1$。$f(0)=1, f(1)=10, f(2)=49$。
入力例 2
3 4
5 0 3
1 2 3 4
出力例 2
8
17
32
53
$f(x) = 3x^2+5$。$f(1)=8, f(2)=17, f(3)=32, f(4)=53$。
概念図: Subproduct Tree と剰余伝播
ヒント(段階的開示)
ヒント1: 方向性
多点評価の高速化は Subproduct Tree(積多項式木)を使う。$M$ 個の評価点から積 $\prod_{j=1}^{M}(x - x_j)$ を分割統治で構築し、$f(x)$ を各リーフで剰余を取ることで評価値を得る。
ヒント2: アプローチ
- Build: 葉 = $(x - x_j)$、内部ノード = 子の積でツリー構築 $O(M \log^2 M)$
- Evaluate: 根から $f$ を分割。各ノードで親の多項式を子で割った剰余を子に渡す $O(M \log^2 M)$
- 葉での剰余 $= f \bmod (x - x_j) = f(x_j)$(剰余定理)
ヒント3: コード骨格
# Subproduct Tree の構築
size = 1
while size < M: size <<= 1
tree = [None] * (2 * size)
for i in range(M):
tree[size + i] = [(-xs[i]) % MOD, 1] # x - x_j
for i in range(M, size):
tree[size + i] = [1] # padding
for i in range(size - 1, 0, -1):
tree[i] = poly_mul(tree[2*i], tree[2*i+1])
# 根から剰余を伝播
tree[1] = poly_div_mod(f, tree[1])[1]
for i in range(1, size):
_, rem_l = poly_div_mod(tree[i], tree[2*i])
tree[2*i] = rem_l
_, rem_r = poly_div_mod(tree[i], tree[2*i+1])
tree[2*i+1] = rem_r
模範解答 (Python)
import sys
MOD = 998244353
def modinv(a, m=MOD): return pow(a, m-2, m)
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 = 3 if not invert else modinv(3)
w = pow(w, (MOD-1)//length, 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 = modinv(n)
for i in range(n): a[i] = a[i]*inv_n%MOD
def poly_mul(a, b):
rl = len(a)+len(b)-1; n = 1
while n < rl: n <<= 1
fa = a+[0]*(n-len(a)); fb = b+[0]*(n-len(b))
ntt(fa, False); ntt(fb, False)
for i in range(n): fa[i] = fa[i]*fb[i]%MOD
ntt(fa, True); return fa[:rl]
def poly_inv(a, deg):
r = [modinv(a[0])]; length = 1
while length < deg:
length <<= 1
ar = poly_mul(a[:length], r)[:length]
rr = poly_mul(r, r)[:length]
arr = poly_mul(a[:length], rr)[:length]
new_r = [(2*r[i] if i < len(r) else 0)%MOD for i in range(length)]
for i in range(len(arr)): new_r[i] = (new_r[i]-arr[i])%MOD
r = new_r[:length]
return r[:deg]
def poly_div_mod(a, b):
if len(a) < len(b): return [0], a[:]
n = len(a)-len(b)+1
a_rev = a[::-1][:n]; b_rev = b[::-1][:n]
q_rev = poly_mul(a_rev, poly_inv(b_rev, n))[:n]
q = q_rev[::-1]
qb = poly_mul(q, b)
r = [(a[i]-(qb[i] if i= len(tree[1]) else f[:]
for i in range(1, size):
if tree[i] is None: continue
_, r = poly_div_mod(tree[i], tree[2*i]); tree[2*i] = r
_, r = poly_div_mod(tree[i], tree[2*i+1]); tree[2*i+1] = r
return [(tree[size+i][0]%MOD if tree[size+i] else 0) for i in range(M)]
def solve():
data = sys.stdin.read().split()
idx = 0
N = int(data[idx]); idx += 1
M = int(data[idx]); idx += 1
f = [int(data[idx+i])%MOD for i in range(N)]; idx += N
xs = [int(data[idx+i])%MOD for i in range(M)]; idx += M
print(*multipoint_eval(f, xs), sep='\n')
solve()
Step-by-Step 解説
Step 1: 剰余定理の応用
任意の多項式 $f(x)$ に対し $f(x) = (x - a) \cdot q(x) + f(a)$ が成立(剰余定理)。よって $f \bmod (x-a) = f(a)$。
Step 2: Subproduct Tree の構築
葉から根に向かって積を計算。深さ $O(\log M)$、各レベルで多項式サイズの和が $O(M)$。NTT を使った多項式乗算 $O(M \log M)$ × $O(\log M)$ レベル = $O(M \log^2 M)$。
Step 3: 剰余の根から葉への伝播
$f$ を根の多項式で割った余り $r$ を計算し、$r$ を左右の子で割った余りを子に渡す。葉まで伝播すると $f(x_j)$ が得られる。
Step 4: 多項式割り算の実装
次数 $n$ の多項式の逆元を Newton 法で計算($O(n \log n)$)し、割り算に使う。全体 $O(M \log^2 M)$。
Step 5: 計算量比較
| 手法 | 計算量 |
|---|---|
| ナイーブ | $O(NM)$ |
| Subproduct Tree | $O((N+M)\log^2(N+M))$ |
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 葉の多項式を $(x_j - x)$ にする | 符号ミス | [(-xs[i]) % MOD, 1] = $(x - x_j)$ |
| 葉の剰余が空リストになる | 次数0の多項式処理 | r[0] if r else 0 |
| NTT の mod 対応を確認しない | $998244353$ 以外では素朴 NTT 不可 | $p = 998244353$ を使用 |
| 多項式逆元の Newton 法バグ | 実装が複雑 | テスト用に手計算で検証 |
次のステップ
- 発展問題: Lagrange 補間の高速化(多点評価 + 剰余木で $O(N \log^2 N)$)
- FPS exp/log との組み合わせ
- Bostan-Mori アルゴリズムとの比較
- 多項式の多点評価 + 内挿の一体実装