問題
$N$ 種類の品物がある。品物 $i$ は重さ $w_i$、価値 $v_i$ で、最大 $c_i$ 個まで使える。重さの合計が $W$ 以下になるように選ぶとき、価値の合計の最大値を求めよ。
入力形式
N W
w_1 v_1 c_1
...
w_N v_N c_N
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le100$, $1\le W\le2\times10^4$ |
| 2 | $1\le w_i\le W$, $1\le v_i\le10^9$, $1\le c_i\le10^9$ |
入力例1
3 10
3 5 2
4 7 1
5 9 3出力例1
18(重さ 5 の品物を 2 個で重さ 10、価値 18。重さ 3 を 2 個と 4 を 1 個でも重さ 10 だが価値は 17)
入力例2
2 100
1 1 1000000000
3 4 5出力例2
105(重さ 3 を 5 個で重さ 15・価値 20、残り 85 を重さ 1 の品物で 85)
入力例3
1 5
2 3 1出力例3
3背景と出典
個数制限付きナップサックの高速化は、(a) 2 の冪で品物を分割して 0-1 ナップサックに帰着する($O(NW\log c)$)、(b) 重さの余り $r$ で列を分け、窓付き最大を単調キューで取る($O(NW)$)、の 2 つが標準的な方法と記憶している(本セッションでは出典を確認していない)。$c_i$ が非常に大きいと $\lfloor W/w_i\rfloor$ が実質の上限になる。
この問題で確かめたいのは次の 3 点。(1) 単調キュー版が、バックトラックと 2 の冪の分割版の両方と一致するか。(2) 窓幅の 1 ずれと、$k\cdot v$ のずらしの欠落が、何件で外れるか。(3) Python で本当に分割版より速いか(理論の $\log c$ 倍の差が実測で出るか)。
ヒント(クリックで段階的に開示)
ヒント1(方向性)
品物 1 種類を処理するとき、新しい表は $\text{new}[j]=\max_{0\le k\le c}\ \text{dp}[j-kw]+kv$。$j$ を $w$ で割った余り $r$ が同じ $j$ だけが関わる。$r$ ごとに $j=r,\ r+w,\ r+2w,\ldots$ と並べると、遷移は「列の中の窓の最大」になる。
ヒント2(アプローチ)
列の $k$ 番目の要素を $f_k=\text{dp}[r+kw]-kv$ とすると、$\text{new}[r+kw]=\max_{k-c\le k'\le k}f_{k'}+kv$。ずらした値 $f$ の窓の最大なので、単調減少のキューで $O(1)$ 償却。
ヒント3(誘導)
キューには $(k, f_k)$ を、$f$ が単調減少になるよう末尾から消しながら積む。先頭の $k$ が $k-c$ より小さければ先頭を捨てる(窓は $k-c$ 以上)。新しい値は先頭の $f$ に $kv$ を足して new[j] に書く。dp は読み取り専用、new に書く(同じ品物を 2 度使わない)。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from collections import deque
def solve(W, items):
dp = [0] * (W + 1) # dp[j] = 重さ j 以下で達成できる最大価値
for w, v, c in items:
new = dp[:]
for r in range(min(w, W + 1)):
dq = deque() # (k, f) 単調減少の f = dp[r+k*w] - k*v
k = 0
j = r
while j <= W:
f = dp[j] - k * v
while dq and dq[-1][1] <= f: dq.pop()
dq.append((k, f))
if dq[0][0] < k - c: dq.popleft() # 使う個数は k-c..k
new[j] = dq[0][1] + k * v
k += 1; j += w
dp = new
return dp[W]
def main():
data = sys.stdin.read().split()
n, W = int(data[0]), int(data[1])
items = [(int(data[2+3*i]), int(data[3+3*i]), int(data[4+3*i])) for i in range(n)]
print(solve(W, items))
if __name__ == "__main__": main()
計算量: 品物 1 種類あたり全 $j$ を 1 回ずつ見て、キューの操作は償却 $O(1)$ なので $O(NW)$。実測(Python 3.14.7、M1 Mac)で $N=100,\ W=2\times10^4,\ c=10^9$、$w$ を $1..1000$ から無作為に選んだとき 1.46 s、$w\le50$ のとき 0.91 s。
Step-by-Step 解説
$\text{new}[j]$ は $\text{dp}[j-kw]$ から作られるので、$j\bmod w$ が同じものだけが関わる。余り $r=0,\ldots,w-1$ ごとに、$j=r,r+w,r+2w,\ldots$ の列を独立に処理する。
列の番号を $k$ とし、$j=r+kw$。$k'=k-t$($0\le t\le c$)の $\text{dp}[r+k'w]$ から $t$ 個使うと価値が $tv=(k-k')v$ 増える。よって $\text{new}[j]=kv+\max_{k-c\le k'\le k}\bigl(\text{dp}[r+k'w]-k'v\bigr)$。$k$ に依存する $kv$ が窓の外に出るので、$f_{k'}=\text{dp}[\cdot]-k'v$ を窓の最大に使い、最後に $kv$ を足す。
窓を右に 1 つ進めるとき、新しい $f_k$ 以下の末尾を捨てる(それらは窓にいる間ずっと $f_k$ に負ける)。先頭は窓から出たら捨てる。先頭が窓の最大。
dp を読んで new に書く。同じ配列に書くと、同じ品物を $c$ 個を超えて使う。1 種類終えたら dp = new。
dp[W] は「重さ $W$ 以下」の最大価値(dp の初期値を全て 0 にしてあるため)。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 余りの類+単調キュー(本解) | $O(NW)$ | 採用 | 500 件のバックトラックと一致、60 件の分割版と一致 |
| 2 の冪で分割して 0-1 ナップサック | $O(NW\log c)$ | 検算用 | 実測で本解の 1.1〜2.7 倍遅い(下のログ)。実装は短く、検算には向く |
| 1 個ずつ展開して 0-1 | $O(W\sum c_i)$ | 不採用 | $c_i=10^9$ で動かない |
| $c_i\ge\lfloor W/w_i\rfloor$ なら完全ナップサック(前から更新) | $O(W)$ | 不採用 | 個数が効かない品物にだけ使える特殊化。本解の窓が実質同じ働きをする |
前提が変わったら:
- $W\le10^9$ で $w_i$ が小さい場合は、ナップサックの表そのものが持てない。価値と重さの比が最大の品物を中心に、他を少数ずつ使う「ほぼ貪欲」の性質を使う方法(Proximity)が要る。実装していない
- 価値が小さく重さが大きい($v_i\le100$ など)なら、dp の添字を価値にした $O(N\cdot V)$ に切り替える
- 品物ごとの個数制限が変化する(クエリで $c_i$ が変わる)場合は、品物の追加・削除に強い構造(分割統治+ロールバック)が要る
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $kv$ のずらしを忘れる | 窓の最大を $\text{dp}$ のまま取る | f = dp[j] - k*v、書き込みで + k*v(352/500 件で誤答) |
| 窓幅が 1 ずれる | 「$c$ 個まで」を $c-1$ 個までにする | 先頭を捨てる条件は k - c 未満(k - c + 1 では 276/500 件で誤答) |
dp を上書きしながら使う | 同じ品物を 2 回以上処理する | new = dp[:] に書き、品物ごとに替える |
| $c_i$ が大きいときの桁あふれ | $kv$ が大きい | Python では起きないが、C++ では 64 ビットで $10^9\times2\times10^4\times10^9$ に注意 |
次のステップ
- 発展1: $c_i$ の合計が $10^5$ 以下で $W\le10^9$ の場合(二分割 + 小さい重さの前処理)を考える
- 発展2: 「ちょうど $W$」でなく、重さの和ごとに「最大価値と、その選び方の個数」を同時に求める。単調キューは同点の個数を持てるか
- 次回予告: 焦点帯 M1 から 3 問以上を含む通常のローテーション
検証ログ(本セッションで実行した数値)
- 照合(実測): $N\le4$、$W\le25$、$w\le9$、$c\le6$ の 500 件で、単調キュー版・バックトラック・2 の冪の分割版の 3 者が一致。$N\le30$、$W\le3000$、$v\le10^9$、$c\in\{1,2,5,10^9\}$ の 60 件は、単調キュー版と分割版が一致
- 誤った式(実測): 窓幅を 1 縮めると 500 件中 276 件、$kv$ を引かないと 352 件で外れる
- 実行時間(実測、$N=100,\ W=2\times10^4,\ c_i=10^9$): $w\in[1,1000]$ で本解 1.46 s/分割版 1.68 s、$w\in[1,50]$ で 0.91 s/2.24 s、$w\in[1,5]$ で 1.24 s/2.39 s。同じ 3 設定で本解と分割版の答えは一致。$W=10^5$ では本解 7.96 s/分割版 21.35 s(計測時の機械負荷でぶれるため目安)
- 入力例(実測): 18、105、3(出力例どおり)
- 扱っていない範囲: $W$ が大きい場合の Proximity による高速化、C++ での実装時間との比較、価値を添字にした DP との切り替えの閾値