E - 宝箱の選択 / Choosing Treasure Chests Editorial by admin
GPT 5.2 High概要
0/1ナップサックの最適価値をDPで求めたうえで、「最適解(複数あり得る)のどれかに含まれる宝箱」を、最適解全体をまとめて逆向きにたどることで判定します。
考察
この問題は典型的な0/1ナップサックですが、求めたいのは最大価値そのものではなく、
- 各宝箱 \(i\) について「宝箱 \(i\) を含む最適解が存在するか」
です。最適解が複数あるのがポイントで、単に1つの最適解を復元して含まれるかを見るだけでは不十分です。
素朴案が厳しい理由
- 各宝箱を「必ず入れる」と固定してナップサックを解き直す、あるいは「除外して」解き直す、などを宝箱ごとに行うと、DPを \(N\) 回回すことになり \(O(N^2M)\) で重いです(\(1000^2 \cdot 3000\) は現実的でない)。
- 最適解を全列挙するのは当然指数時間で不可能です。
解決の方向性
まず通常のDPで「最適価値」をすべての \(i,w\) について計算します(表DP)。 そのあと、DP表上で「最適解に対応する遷移だけ」を逆向きにたどり、最適解になり得る状態を全部まとめて管理します。
DP表は有向非巡回グラフ(DAG)のように見なせます: - 状態 \((i,w)\) から \((i-1,w)\)(宝箱 \(i\) を入れない) - 状態 \((i,w)\) から \((i-1,w-A_i)\)(宝箱 \(i\) を入れる) のどちらか(または両方)が、最適値を保つなら「最適解に沿った遷移」です。
この「最適遷移」を逆にたどっていき、どこかの最適解の経路に 一度でも「入れる遷移」が現れた宝箱は Yes になります。
アルゴリズム
1. DPで最大価値を計算
\(dp[i][w]\) を - 「先頭から \(i\) 個の宝箱だけを使って、容量 \(w\) 以下で得られる最大価値」 とします。
遷移は0/1ナップサックそのものです:
- 入れない:\(dp[i][w] = dp[i-1][w]\)
- 入れる(\(w \ge A_i\)):\(dp[i][w] = \max(dp[i][w],\ dp[i-1][w-A_i] + B_i)\)
これで最適価値は \(dp[N][M]\) です。
2. 「最適解になり得る状態」を逆向きに同時追跡
最適解が複数あるので、1本の復元ではなく、到達可能な状態集合を持ちます。
reach[w] = 1を「現在の段階の状態 \((i,w)\) が、(N,M) から最適遷移だけを辿って到達可能」を表すとします。- 初期は \((N,M)\) だけなので
reach[M]=1。
\(i=N,N-1,\dots,1\) の順に、各到達状態 \((i,w)\) から「最適遷移」だけを \((i-1,*)\) へ伸ばします:
- 入れない遷移が最適なら:
- 条件:\(dp[i][w] = dp[i-1][w]\)
- 次も到達可能:\((i-1,w)\)
- 入れる遷移が最適なら:
- 条件:\(w\ge A_i\) かつ \(dp[i][w] = dp[i-1][w-A_i] + B_i\)
- 次も到達可能:\((i-1,w-A_i)\)
- さらに「宝箱 \(i\) を含む最適解が存在」するので
included[i]=True
このようにすると、「(N,M) から始まる最適解の経路」を全てまとめて辿れるため、各宝箱について「含む最適解があるか」を漏れなく判定できます。
計算量
- 時間計算量: \(O(NM)\)
- DP作成が \(O(NM)\)、逆向き到達判定も \(O(NM)\)
- 空間計算量: \(O(NM)\)
- \(dp\) 表を全段保持するため \(O(NM)\)(復元に必要)
実装のポイント
価値の合計は最大で \(1000 \cdot 10^9 = 10^{12}\) なので 64bit 整数が必要です(Pythonでは通常intでOKですが、高速化のため
array('q')を使用)。逆向き探索の到達集合
reachは、長さ \(M+1\) のbytearrayにすると高速かつ省メモリです。逆向きでは「等号チェック」で最適遷移かどうかを判断します:
\(dp[i][w] == dp[i-1][w]\)(入れないでも最適)
\(dp[i][w] == dp[i-1][w-A_i] + B_i\)(入れるのが最適) 両方成り立つ場合もあり、そのときは両方の遷移を残すのが重要です(最適解が分岐するため)。
ソースコード
import sys
from array import array
def main():
data = list(map(int, sys.stdin.buffer.read().split()))
N, M = data[0], data[1]
A = [0] * (N + 1)
B = [0] * (N + 1)
p = 2
for i in range(1, N + 1):
A[i] = data[p]
B[i] = data[p + 1]
p += 2
# dp[i][w] = max value using first i items with capacity w
dp = [array('q', [0]) * (M + 1)]
for i in range(1, N + 1):
a = A[i]
b = B[i]
prev = dp[i - 1]
cur = array('q', prev) # exclude by default
for w in range(a, M + 1):
v = prev[w - a] + b
if v > cur[w]:
cur[w] = v
dp.append(cur)
included = [False] * (N + 1)
# reachable capacities at step i (state (i, w)) along some optimal path from (N, M)
reach = bytearray(M + 1)
reach[M] = 1
for i in range(N, 0, -1):
a = A[i]
b = B[i]
cur = dp[i]
prev = dp[i - 1]
next_reach = bytearray(M + 1)
r = reach
nr = next_reach
for w in range(M + 1):
if not r[w]:
continue
v = cur[w]
if v == prev[w]:
nr[w] = 1
if w >= a and v == prev[w - a] + b:
included[i] = True
nr[w - a] = 1
reach = next_reach
out = []
for i in range(1, N + 1):
out.append("Yes" if included[i] else "No")
sys.stdout.write("\n".join(out))
if __name__ == "__main__":
main()
この解説は gpt-5.2-high によって生成されました。
posted:
last update: