Skip to content

ナップサック問題の最適解復元

概要

ナップサック問題の DP で最適値(価値の最大値)だけでなく、実際にどの品物を選んだかを復元する方法。

動的計画法の最適解復元の方法 2(伝播元メモ方式)を適用する。

ナップサック問題

\(n\) 個の品物があり、\(i\) 番目の品物の重さを \(weight[i]\)、価値を \(value[i]\) とする。 重さの総和が \(W\) を超えないように選んだときの、価値の総和の最大値を求め、さらに選んだ品物を列挙する。

制約 - \(1 \leq n \leq 100\) - \(1 \leq weight[i], value[i] \leq 1000\) - \(1 \leq W \leq 10000\)

数値例 \(n=6\), \((weight, value) = (2,3),(1,2),(3,6),(2,1),(1,3),(5,85)\), \(W=8\) 答え: \(91\)(品物 \((3,6)\)\((5,85)\) を選ぶ)

DP テーブルの定義

\[dp[i][w] := \text{最初の } i \text{ 個の品物から重さが } w \text{ 以下になるよう選んだ価値の最大値}\]

DP 更新式

品物 \(i\)(0-indexed)について:

  • 品物 \(i\) を選ぶ場合 (\(w \geq weight[i]\)): \(dp[i+1][w] = \max(dp[i+1][w],\ dp[i][w - weight[i]] + value[i])\)

  • 品物 \(i\) を選ばない場合: \(dp[i+1][w] = \max(dp[i+1][w],\ dp[i][w])\)

ナップサックDP遷移図

出典: 意外と解説がない!動的計画法で得た最適解を「復元」する一般的な方法

復元用テーブル

int prev_w[110][10010];

prev_w[i+1][w] = dp[i+1][w]dp[i][prev_w[i+1][w]] から更新されたことを示す。

  • 品物 \(i\)選んだ場合: prev_w[i+1][w] = w - weight[i]
  • 品物 \(i\)選ばなかった場合: prev_w[i+1][w] = w

完全な実装 (C++)

#include <iostream>
using namespace std;

int n, W;
int weight[110], value[110];

int dp[110][10010] = {0};
int prev_w[110][10010];

int main() {
    cin >> n >> W;
    for (int i = 0; i < n; ++i) cin >> value[i] >> weight[i];

    // DP ループ
    for (int i = 0; i < n; ++i) {
        for (int w = 0; w <= W; ++w) {

            // 品物 i を選ぶ場合
            if (w >= weight[i]) {
                if (dp[i+1][w] < dp[i][w-weight[i]] + value[i]) {
                    dp[i+1][w] = dp[i][w-weight[i]] + value[i];
                    prev_w[i+1][w] = w - weight[i];  // 伝播元をメモ
                }
            }

            // 品物 i を選ばない場合
            if (dp[i+1][w] < dp[i][w]) {
                dp[i+1][w] = dp[i][w];
                prev_w[i+1][w] = w;  // 重さ変化なし
            }
        }
    }

    cout << dp[n][W] << endl;

    // 復元
    cout << "Selected items are..." << endl;
    int cur_w = W;
    for (int i = n-1; i >= 0; --i) {
        // 重さが変化していれば品物 i を選んでいた
        if (prev_w[i+1][cur_w] == cur_w - weight[i]) {
            cout << i << " th item (weight = " << weight[i]
                 << ", value = " << value[i] << ")" << endl;
        }
        cur_w = prev_w[i+1][cur_w];
    }
}

出力例

91
Selected items are...
5 th item (weight = 5, value = 85)
4 th item (weight = 1, value = 3)
0 th item (weight = 2, value = 3)

復元ロジックの解説

prev_w[i+1][cur_w] == cur_w - weight[i] が成り立てば、品物 \(i\)選んでいたことがわかる。 等しくなければ prev_w[i+1][cur_w] == cur_w であり、品物 \(i\)選ばなかった

引用元: 意外と解説がない!動的計画法で得た最適解を「復元」する一般的な方法