D - 宝石の選択 / Selection of Gems 解説 by admin
Claude 4.6 Opus (Thinking)概要
重量制限付きの0/1ナップサック問題に「\(K\)個の軽量化ケースで宝石の重さを半減できる」という追加条件が加わった問題です。ケースの使用個数を追加次元としたDPで解きます。
考察
基本的な気づき
この問題は通常の0/1ナップサック問題の拡張です。各宝石に対して以下の3つの選択肢があります:
- 持ち込まない
- そのまま持ち込む(重さ \(W_i\))
- 軽量化ケースを使って持ち込む(重さ \(\lfloor W_i / 2 \rfloor\)、ケースを1個消費)
素朴なアプローチの問題
単純に「どの宝石にケースを使うか」を全探索すると \(\binom{N}{K}\) 通りあり、\(N\) が大きい場合に爆発します。
解決方針
ケースの使用個数を状態に組み込んだDPを行います。\(dp[k][c]\) を「軽量化ケースを \(k\) 個まで使い、総重量 \(c\) 以内で得られる最大価値」と定義すれば、各宝石を1つずつ処理する標準的なナップサックの要領で解けます。
アルゴリズム
DP定義
\[dp[k][c] = \text{ケースを最大 } k \text{ 個使い、容量 } c \text{ 以内で得られる最大価値}\]
遷移
各宝石 \((B_i, W_i)\)(半減重量 \(hw_i = \lfloor W_i/2 \rfloor\))に対して、\(k\) を \(K\) から \(0\) まで、\(c\) を \(C\) から降順に走査し:
そのまま持ち込む(\(c \geq W_i\) のとき): $\(dp[k][c] \leftarrow \max(dp[k][c],\ dp[k][c - W_i] + B_i)\)$
ケースを使って持ち込む(\(k > 0\) かつ \(c \geq hw_i\) のとき): $\(dp[k][c] \leftarrow \max(dp[k][c],\ dp[k-1][c - hw_i] + B_i)\)$
\(k\) と \(c\) を降順に処理することで、同じ宝石を複数回選ぶことを防ぎます(0/1ナップサックの標準テクニック)。
具体例
例:\(N=2, C=10, K=1\)、宝石1: \((B=100, W=8)\)、宝石2: \((B=80, W=6)\)
- 宝石1にケースを使うと重さ4に → 宝石1(重さ4) + 宝石2(重さ6) = 重さ10で価値180
- ケースなしでは重さ8+6=14 > 10 で両方は持てない
DPが最適な割り当てを自動的に見つけます。
計算量
- 時間計算量: \(O(N \cdot (K+1) \cdot C)\)
- 制約 \(N(K+1)(C+1) \leq 2 \times 10^7\) により実行時間内に収まる
- 空間計算量: \(O((K+1) \cdot C)\)
実装のポイント
降順ループ: 0/1ナップサックなので、容量 \(c\) もケース数 \(k\) も大きい方から更新します。これにより同じアイテムを重複選択することを防ぎます。
場合分けによる高速化: \(W_i > C\) の場合、ケースなしでは絶対に入らないため、ケース使用の遷移のみを行います。また、\(W_i \leq C\) の場合でも、\(hw_i \leq c < W_i\) の範囲ではケース使用の遷移のみが有効なので、ループを分割して無駄な比較を減らしています。
ビットシフト: \(\lfloor W_i / 2 \rfloor\) を
w >> 1で計算し、わずかに高速化しています。Python での速度対策:
sys.stdin.buffer.read()で入力を一括読み込みし、DPテーブルの行を変数(dpk,dpk1)に束縛することで属性アクセスのオーバーヘッドを削減しています。ソースコード
import sys
def main():
data = sys.stdin.buffer.read().split()
idx = 0
N = int(data[idx]); idx += 1
C = int(data[idx]); idx += 1
K = int(data[idx]); idx += 1
items = []
for i in range(N):
b = int(data[idx]); idx += 1
w = int(data[idx]); idx += 1
items.append((b, w))
# dp[k][c] = max value using at most k cases, weight at most c
dp = [[0] * (C + 1) for _ in range(K + 1)]
for b, w in items:
hw = w >> 1
for k in range(K, -1, -1):
dpk = dp[k]
if k > 0:
dpk1 = dp[k - 1]
if w <= C:
for c in range(C, w - 1, -1):
val = dpk[c]
nv = dpk[c - w] + b
if nv > val:
val = nv
nv = dpk1[c - hw] + b
if nv > val:
val = nv
dpk[c] = val
upper = min(w - 1, C)
for c in range(upper, hw - 1, -1):
nv = dpk1[c - hw] + b
if nv > dpk[c]:
dpk[c] = nv
else:
if hw <= C:
for c in range(C, hw - 1, -1):
nv = dpk1[c - hw] + b
if nv > dpk[c]:
dpk[c] = nv
else:
if w <= C:
for c in range(C, w - 1, -1):
nv = dpk[c - w] + b
if nv > dpk[c]:
dpk[c] = nv
print(dp[K][C])
main()
この解説は claude4.6opus-thinking によって生成されました。
投稿日時:
最終更新: