D - お土産選び / Choosing Souvenirs Editorial by admin
GPT 5.2 High概要
在庫数制限つきのナップサック(重さは「ちょうど \(S\)」)で、達成可能なら満足度合計の最大値を求める問題です。到達不能なら -1 を出力します。
考察
これは典型的な 個数制限付き(bounded)ナップサックです。
状態として「重さ \(x\) を作るときの満足度最大」を持てばよいですが、素朴に
- 各商品 \(i\) について
- 現在の重さ \(x\) について
- 個数 \(k=0..c_i\) を試す
とすると、計算量が \(O\!\left(\sum_i S\cdot c_i\right)\) になり、\(c_i\) が最大 \(10^5\) なので確実に TLE します。
そこで、各商品の更新 [ dp{\text{new}}[x] = \max{0\le t\le c}{dp[x-tw] + tv} ] を高速化します。ポイントは、重さ \(x\) を \(w\) で割った 余りごとに分けることです。
\( x = r + kw \ (0\le r < w)\) と書くと [ dp{\text{new}}[r+kw] = \max{0\le t\le c}{dp[r+(k-t)w] + tv}. ] ここで \(j=k-t\) と置くと(つまり「前の状態の段数」) [ dp{\text{new}}[r+kw] = \max{k-c \le j \le k}{dp[r+jw] + (k-j)v}. ] さらに整理して [ dp{\text{new}}[r+kw] = kv + \max{k-c \le j \le k}{dp[r+jw] - jv}. ] つまり、各 \(k\) で必要なのは区間 \([k-c,\,k]\) の [ A_j = dp[r+jw] - jv ] の最大値です。これは スライディングウィンドウ最大なので、単調キュー(deque)で \(O(1)\) amortized で更新でき、各商品あたり全体で \(O(S)\) にできます。
アルゴリズム
- \(dp[x]\) を「重さ合計がちょうど \(x\) のときの満足度最大」とする。
- 到達不能は十分小さい値(コードでは
NEG)で表す。 - 初期値:\(dp[0]=0\)、それ以外は
NEG。
- 到達不能は十分小さい値(コードでは
- 各商品 \((w,v,c)\) について次を行う:
- 新しい配列 \(dp\_new\) を
NEGで初期化。 - 余り \(r=0..w-1\) ごとに、\(x=r+kw\)(\(k=0,1,2,\dots\))を昇順に走査する。
- deque には候補 \(j\) とその値 \(A_j = dp[r+jw]-jv\) を「値が大きい順」になるように保持する。
- \(k\) を進めるたびに:
- 現在の \(j=k\) を候補として deque に追加(到達可能なときのみ)。
- 窓の範囲外(\(j < k-c\))を deque の先頭から削除。
- deque の先頭が \(\max A_j\) なので [ dp_new[r+kw] = (\max A_j) + kv ] とできる。
- 新しい配列 \(dp\_new\) を
- 最後に \(dp[S]\) が
NEGなら-1、そうでなければその値を出力。
この方法により、「個数 \(0..c\) を全部試す」部分を、余りごとの単調キューでまとめて処理できます。
計算量
- 時間計算量: \(O(N\cdot S)\)
(各商品につき、\(0..S\) の各重さを高々定数回 deque 操作で処理) - 空間計算量: \(O(S)\)
(\(dp\) と \(dp\_new\) の2本)
実装のポイント
到達不能の管理が重要です。
NEG(十分小さい値)を使い、dp[pos] != NEGのときだけ deque に追加します。余り \(r\) ごとに \(pos=r+kw\) を増やすことで、「同じ余りの列」だけを見ればよくなり、式変形が成立します。
deque には
(j, A_j)を入れ、末尾から「値が小さい(または同じ)候補」を削って単調性を保ちます(最大値が先頭に来る)。
窓幅は「最大 \(c\) 個まで」なので、\(j < k-c\) を先頭から捨てることで個数制限を守ります。
ソースコード
import sys
from collections import deque
def main():
input = sys.stdin.buffer.readline
N, S = map(int, input().split())
items = [tuple(map(int, input().split())) for _ in range(N)]
NEG = -10**18
dp = [NEG] * (S + 1)
dp[0] = 0
for w, v, c in items:
dp_new = [NEG] * (S + 1)
for r in range(w):
dq = deque()
k = 0
while True:
pos = r + k * w
if pos > S:
break
base = dp[pos]
if base != NEG:
cand = base - k * v
while dq and dq[-1][1] <= cand:
dq.pop()
dq.append((k, cand))
lim = k - c
while dq and dq[0][0] < lim:
dq.popleft()
if dq:
dp_new[pos] = dq[0][1] + k * v
k += 1
dp = dp_new
ans = dp[S]
print(ans if ans != NEG else -1)
if __name__ == "__main__":
main()
この解説は gpt-5.2-high によって生成されました。
posted:
last update: