Day 065-Q3 — 多次元DP + Roll最適化(次元削減・メモリ圧縮)

2026-06-18 赤色 Master / Phase 8+ ★★★★★★★★★ 2D Knapsack / Roll Optimization / 個数制約 / メモリ効率

問題

$N$ 個のアイテムがあり、アイテム $i$ は重さ $w_i$、体積 $v_i$、価値 $p_i$ を持つ。重さ上限 $W$、体積上限 $V$ のナップサックに入れるとき:

  1. 選択個数制限なし: 価値の最大値
  2. 選択個数がちょうど $K$ 個: 価値の最大値

2つの値を出力せよ。メモリ使用量を $O(W \times V)$($N$ に依存しない) に抑えた Roll 最適化を実装すること。

制約

パラメータ範囲
$N$$1 \le N \le 500$
$W, V$$1 \le W, V \le 1000$
$K$$1 \le K \le N$
$w_i, v_i, p_i$$0 \le w_i \le W$, $0 \le v_i \le V$, $0 \le p_i \le 10^9$

入出力例

入力例 1

3 10 10 2
3 4 5
4 3 8
5 6 10

出力例 1

18
18

アイテム2+3: 重さ9, 体積9, 価値18。ちょうど2個でも同じ組み合わせが最適。

概念図: Roll最適化のメモリ削減

通常の3D DP テーブル: O(N × W × V) dp[0..N][0..W][0..V] 500 × 1000 × 1000 = 5×10^8 cells → メモリ超過! Roll 最適化後: O(W × V) dp[0..W][0..V] 1000 × 1000 = 10^6 cells → 500倍削減! Roll の核心 遷移: dp[j][k] は 直前の状態 dp[j-wi][k-vi] のみ参照する。 ⇒ 逆順ループで実現 更新順序(0/1 ナップサック: 逆順) for j in range(W, wi-1, -1): # 重さを降順 for k in range(V, vi-1, -1): # 体積を降順 dp[j][k] = max(dp[j][k], dp[j-wi][k-vi] + pi) # 旧値を参照

ヒント(段階的開示)

ヒント1: 方向性
3次元DP dp[i][j][k] = 最初の $i$ 個、重さ $j$、体積 $k$ を使った場合の最大価値。これを Roll(ローリング)最適化で $i$ 軸を削除し $O(W \times V)$ メモリで実現する。更新を逆順(降順)で行うことがポイント。
ヒント2: 個数制約付きDP
dp_k[cnt][j][k] = ちょうど cnt 個使用、重さ $j$、体積 $k$ の最大価値。cnt ループも逆順にすることで同一アイテムの多重カウントを防ぐ。初期値は-INF(実現不可能を表す)、dp_k[0][0][0] = 0
ヒント3: コード骨格
INF = float('-inf')

# Part 1: 2D Knapsack with Roll
dp = [[INF]*(V+1) for _ in range(W+1)]
dp[0][0] = 0
for wi, vi, pi in items:
    for j in range(W, wi-1, -1):      # 逆順必須
        for k in range(V, vi-1, -1):  # 逆順必須
            if dp[j-wi][k-vi] != INF:
                dp[j][k] = max(dp[j][k], dp[j-wi][k-vi]+pi)

# Part 2: K個制約
dp_k = [[[INF]*(V+1) for _ in range(W+1)] for _ in range(K+1)]
dp_k[0][0][0] = 0
for wi, vi, pi in items:
    for cnt in range(K, 0, -1):        # cntも逆順
        for j in range(W, wi-1, -1):
            for k in range(V, vi-1, -1):
                if dp_k[cnt-1][j-wi][k-vi] != INF:
                    dp_k[cnt][j][k] = max(...)

模範解答 (Python)

import sys
input = sys.stdin.readline

def solve():
    N, W, V, K = map(int, input().split())
    items = []
    for _ in range(N):
        wi, vi, pi = map(int, input().split())
        items.append((wi, vi, pi))

    INF = float('-inf')

    # Part 1: 2D Knapsack with Roll optimization O(W*V) memory
    dp = [[INF] * (V + 1) for _ in range(W + 1)]
    dp[0][0] = 0

    for wi, vi, pi in items:
        for j in range(W, wi - 1, -1):
            for k in range(V, vi - 1, -1):
                if dp[j - wi][k - vi] != INF:
                    dp[j][k] = max(dp[j][k], dp[j - wi][k - vi] + pi)

    ans1 = max(dp[j][k] for j in range(W+1) for k in range(V+1) if dp[j][k] != INF)

    # Part 2: 2D Knapsack with exact K items
    dp_k = [[[INF] * (V + 1) for _ in range(W + 1)] for _ in range(K + 1)]
    dp_k[0][0][0] = 0

    for wi, vi, pi in items:
        for cnt in range(min(K, N), 0, -1):   # 逆順でcntループ
            for j in range(W, wi - 1, -1):
                for k in range(V, vi - 1, -1):
                    if dp_k[cnt - 1][j - wi][k - vi] != INF:
                        dp_k[cnt][j][k] = max(
                            dp_k[cnt][j][k],
                            dp_k[cnt - 1][j - wi][k - vi] + pi
                        )

    candidates = [dp_k[K][j][k] for j in range(W+1) for k in range(V+1)
                  if dp_k[K][j][k] != INF]
    ans2 = max(candidates) if candidates else 0

    print(ans1)
    print(ans2)

solve()

Step-by-Step 解説

Step 1: 2D ナップサックの状態空間

dp[j][k] = 重さ $j$ 以下、体積 $k$ 以下で実現できる最大価値。遷移は $O(N \times W \times V)$ で全体の計算量は変わらない。

Step 2: Roll 最適化の核心

通常の3Dテーブル dp[i][j][k] は $O(N \times W \times V)$ メモリを要する。しかし遷移で使うのは「直前の $i-1$ 行」のみ。更新を逆順(降順)で行うことで、未更新の旧値を参照し続けられる。

Step 3: 個数制約付き DP

dp_k[cnt][j][k] = ちょうど cnt 個使用、重さ $j$、体積 $k$ の最大価値。個数ループ cnt逆順にすることで同一アイテムを複数回選ぶ問題を防ぐ(0/1 制約の保証)。

Step 4: 複数制約への拡張

次元Roll後のメモリ
重さのみ$O(W)$
重さ + 体積$O(W \times V)$
重さ + 体積 + 個数$O(K \times W \times V)$

Step 5: 計算量

処理計算量
Part 1 時間$O(N \times W \times V)$
Part 1 空間$O(W \times V)$
Part 2 時間$O(N \times K \times W \times V)$
Part 2 空間$O(K \times W \times V)$

よくあるミス

ミス原因正しい書き方
逆順ループを忘れる アイテムの多重選択が起きる range(W, wi-1, -1) で降順に
初期値を 0 にする 「実現不可能」状態と区別できない -INF で初期化、dp[0][0] = 0
cnt ループも逆順を忘れる 同一アイテムが複数回カウントされる range(K, 0, -1) で降順に
ans2 の候補がゼロ件 ちょうど K 個選べない入力 max(...) if candidates else 0

次のステップ

発展問題: 同種アイテムが複数個あり「アイテム種 $i$ を最大 $c_i$ 個まで」選べる有界ナップサックを、Binary Grouping または Monotone Queue Deque で $O(N \times W \times V \times \log(\max c_i))$ に最適化せよ。

自己評価