Day 161-Q4 — 2026-09-26 — 戻す DP(部分和の数え上げから品物を 1 つ取り除く=多項式 $1+x^{w}$ での割り算)

2026-09-26 赤色 Master (Phase 8+) ★★★★★★★★★ DP 高速化(焦点帯 M1)。「全員で数えた表」から 1 人分を $O(W)$ で引き算し、$N$ 回の作り直し $O(N^2W)$ を $O(NW)$ にする

問題

$N$ 個の品物があり、品物 $i$ の重さは $w_i$。各 $i=1,\dots,N$ について次の値を求めよ。

品物 $i$ 以外 の $N-1$ 個から部分集合を選び、重さの合計がちょうど $W$ になる選び方の数(998244353 で割った余り)

品物は重さが同じでも区別する。空集合も部分集合に含める($W\ge1$ なので答えには効かない)。

入力形式

N W
w_1 w_2 ... w_N

制約

#条件
1$1\le N\le 2000$
2$1\le W\le 2000$
3$1\le w_i\le 10^9$

入出力例

入力例1

6 10
1 2 3 4 5 6

出力例1

2
3
2
2
3
3

入力例2

4 5
1 2 3 4

出力例2

1
1
1
1

(例1: 全体で和が 10 になる部分集合は $\{4,6\},\{1,3,6\},\{1,4,5\},\{2,3,5\},\{1,2,3,4\}$ の 5 個。品物 1(重さ 1)を除くと $\{4,6\},\{2,3,5\}$ の 2 個、品物 2 を除くと $\{4,6\},\{1,3,6\},\{1,4,5\}$ の 3 個)

概念図:戻す DP の向き

w = 1..6, W = 10 から重さ 2 の品物を戻す: g[s] = f[s] − g[s−2](小さい s から) s=0 s=1 s=2 s=3 s=4 s=5 s=6 s=7 s=8 s=9 s=10 f(全部) 1 1 1 2 2 3 4 4 4 5 5 g(重さ 2 を除く) 1 1 0 1 2 2 2 2 2 3 3 g[s−2] は既に戻し終えた値 → 左から右へ(追加のループとは逆向き) 例: g[10] = f[10] − g[8] = 5 − 2 = 3({4,6} {1,3,6} {1,4,5}) max の DP には引き算が無いので戻せない([0,0] と [0,1] が同じ [0,1] になる)

ヒント(クリックで段階的に開示)

ヒント1(方向性)

品物 $i$ ごとに DP を作り直すと $O(N^2W)$。$N=W=2000$ で $8\times10^9$ 回の更新になる(実測で $N=W=1000$ でも 70.9 s)。全員分の表 $f$ を 1 回作り、そこから品物 $i$ の寄与だけを「取り消す」ことを考える。

ヒント2(アプローチ)

部分和の数え上げ表 $f$ は多項式 $F(x)=\prod_i(1+x^{w_i})$ の係数。品物 $i$ を除いた表 $g$ は $G(x)=F(x)/(1+x^{w_i})$。割る式は定数項が 1 なので、係数を小さい次数から順に決められる。

ヒント3(誘導)

$F=G\cdot(1+x^w)$ の係数を比べると $f[s]=g[s]+g[s-w]$。よって

g = f[:]
for s in range(w, W + 1):      # 小さい s から: g[s-w] は既に「戻した後」の値
    g[s] = (g[s] - g[s - w]) % MOD

追加のループ(大きい $s$ から)と 向きが逆 になる。

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys

MOD = 998244353


def main():
    data = sys.stdin.buffer.read().split()
    n, W = int(data[0]), int(data[1])
    w = list(map(int, data[2:2 + n]))
    # 1) 全品物での数え上げ f[s] = 和が s になる部分集合の数(多項式 Π(1+x^{w_i}) の係数)
    f = [0] * (W + 1)
    f[0] = 1
    for wi in w:
        if wi > W:
            continue                         # W を超える品物は和 W に使えない(掛けても係数は変わらない)
        for s in range(W, wi - 1, -1):      # 大きい s から: 同じ品物を 2 回使わない
            f[s] = (f[s] + f[s - wi]) % MOD
    # 2) 品物 i を「戻す」: g * (1+x^{w_i}) = f  ⇒  g[s] = f[s] - g[s-w_i](小さい s から)
    out = []
    for wi in w:
        if wi > W:
            out.append(f[W])
            continue
        g = f[:]
        for s in range(wi, W + 1):
            g[s] = (g[s] - g[s - wi]) % MOD
        out.append(g[W])
    print("\n".join(map(str, out)))


main()

計算量: 表の作成 $O(NW)$、各品物の取り消し $O(W)$、合計 $O(NW)$。実測で $N=W=2000$ が 0.53 s。

Step-by-Step 解説

1部分和 DP は多項式の掛け算

「品物 $i$ を選ぶ/選ばない」は $(1+x^{w_i})$ を掛けることに当たる。$x^s$ の係数が「和が $s$ の選び方の数」。追加のループを大きい $s$ から回すのは、同じ品物を 2 回掛けないため(小さい $s$ からだと $(1+x^w+x^{2w}+\dots)$ を掛けたことになり、個数制限なしのナップサックになる)。

2掛け算の取り消し=割り算

$F=G(1+x^w)$ の $x^s$ の係数は $f[s]=g[s]+g[s-w]$($s<w$ なら $g[s-w]=0$)。$s$ の小さい順に $g[s]=f[s]-g[s-w]$ で一意に決まる。割る多項式 $1+x^w$ の定数項が 1 なので 逆元は要らない。$\bmod p$ のもとでも整数のときと同じ式がそのまま成り立つ。

3ループの向き

追加は「大きい $s$ から」、取り消しは「小さい $s$ から」。取り消しで大きい方から回すと、$g[s-w]$ にまだ取り消し前の $f$ の値が入ったまま引いてしまう。実測で 300 ケース中 173 ケースが誤答になった。

4なぜ「足し算の数え上げ」だから戻せるのか

取り消しは「掛けた多項式で割る」ことで、数え上げ(和と積)では逆演算がある。最大値を求める DP(価値最大のナップサック)では戻せない: $\max$ には逆演算が無く、たとえば重さ 1・価値 1 の品物を追加した後の表 $f=[0,1]$ は、追加前の表が $[0,0]$ でも $[0,1]$ でも同じになる(どちらも $f[1]=\max(g[1],g[0]+1)=1$)。削除が要る最大化は Day158-Q5 の時間軸セグメント木のように「追加だけで済むように並べ替える」。

5素直な方法との比較(実測)
$N=W$戻す DP作り直し $O(N^2W)$
3000.06 s2.27 s
10000.19 s70.9 s
20000.53 s未計測(約 9〜10 分と見積もり)

迷った他案と採らなかった理由

案計算量採否理由
品物ごとに作り直す$O(N^2W)$検算用のみ$N=W=1000$ で 70.9 s(実測)
前からの表と後ろからの表を持ち、品物 $i$ の前後を畳み込む$O(NW)$ 前計算 + 各 $i$ で $O(W)$(和 $W$ の 1 係数だけなら)不採用最大化 DP でも使える汎用の方法だが、表を $2N$ 本($O(NW)$ メモリ、$N=W=2000$ で 800 万要素)持つ。数え上げなら戻す DP は $O(W)$ メモリで済む
分割統治(左半分を足して右へ、右半分を足して左へ再帰)$O(NW\log N)$不採用最大化にも使え、メモリも $O(W\log N)$。数え上げでは $\log N$ 倍遅いだけ
戻す DP(本解)$O(NW)$、メモリ $O(W)$採用数え上げ(逆演算のある演算)に限って最速

前提が変わったら:

  • 重さ 0 の品物 を許すと $(1+x^0)=2$ で割ることになり、$2$ の逆元を掛ける必要がある(998244353 は奇素数なので可能)。法が $2^{64}$ などなら割れない
  • 確率の DP(品物 $i$ を確率 $p_i$ で選ぶ)では $(1-p_i)+p_ix^{w_i}$ で割る。定数項 $1-p_i$ が 0 に近いと数値的に不安定になり、$p_i=1$ では割れない
  • 最大化・最小化 の DP では戻せない(Step 4)。前後の表か分割統治を使う

よくあるミス

ミス原因正しい書き方
取り消しを大きい $s$ から回す$g[s-w]$ に取り消し前の値が残る小さい $s$ から(実測で 300 中 173 ケース誤答)
引き算の後に % MOD をしないPython では負の数のまま残り、次の引き算で壊れる(C++ ならオーバーフロー)(g[s] - g[s-w]) % MOD
$w_i>W$ の品物を添字計算にそのまま使う固定長配列の言語では範囲外参照(Python は range が空になり偶然動く)$w_i>W$ は表を変えない品物として明示的に飛ばし、答えは $f[W]$
$f$ を上書きして取り消す次の品物の取り消しで元の表が要るg = f[:] でコピー
最大価値ナップサックで同じことをする$\max$ に逆演算が無い前後の表・分割統治・時間軸セグ木

次のステップ

  • 発展1: 「品物 $i$ と $j$ の両方を除いた」答えを $Q$ 個のクエリで求める(2 回割る。$O(QW)$)
  • 発展2: 個数制限なしナップサックで品物を 1 種類除く(掛けたのは $1/(1-x^w)$ なので、取り消しは $(1-x^w)$ を掛ける=$g[s]=f[s]-f[s-w]$ を大きい $s$ から)
  • 扱わなかった範囲: 形式的冪級数の $\log$/$\exp$ を使って $\prod(1+x^{w_i})$ を $O(W\log W)$ で求める方法(本問は $O(NW)$ で十分)

検証ログ(本セッションで実行した数値)

  • 全探索との一致(実測): $N\le11$、$W\le25$ のランダム 300 ケースで、プログラム全体(標準入出力)の出力が部分集合の全列挙と全件一致
  • 取り消しの向きを逆にした誤り(実測): 同じ条件の 300 ケース中 173 ケースで不一致
  • 作り直し版との一致と時間(実測): $N=W=300$ で 0.06 s vs 2.27 s、$N=W=1000$ で 0.19 s vs 70.9 s(出力は全件一致)、$N=W=2000$ の本解 0.53 s
  • 入力例1・2 の出力を実行で確認(2 3 2 2 3 3、1 1 1 1)
  • 最大化で戻せない例(導出): 重さ 1・価値 1 の品物について、追加前 $[0,0]$ と $[0,1]$ が同じ $[0,1]$ になる
  • 出所ラベル: 数値は実測(Python 3.14.7)。$N=W=2000$ の作り直し版の時間は $1000$ の結果を 8 倍した見積もり
  • 検証スクリプト: スクラッチパッドの d161/q4.py・q4_test.py・q4_bench.py

自己評価

自分の回答

気づき・メモ