---
title: ナップサック問題
tags: []
categories: ["Programming", "Algorithm", "DynamicProgramming", "Knapsack"]
date: 2011-07-24T12:01:57Z
updated: 2011-07-24T12:01:57Z
---

動的計画法の基礎練。いろいろなパターンのナップサック問題を解く。

### 基本版

一番オーソドックスなやつ。

> 重さと価値が`wi, vi`である`N`個の品物がある。これらの品物から、重さの総和が`W`を超えないように選んだときの価値の総和の最大値はいくつか？ただし、各品物は1つずつしかない。(`1<=N<=100, 1<=wi,vi<=100, 1<=W<=10000`)

`i`番目までの品物から重さの総和が`j`以下となるように選んだ場合の価値の総和の最大値を`dp[i+1][j]`としたとき、

    dp[i + 1][j] = dp[i][j] (j < wi)
                   max(dp[i][j], dp[i][j - wi] + vi)

が成り立つので、

    public class Knapsack1 {
    
        public static void main(String[] args) {
            int[] w = { 2, 1, 3, 2 };
            int[] v = { 3, 2, 4, 2 };
            int W = 5;
            int N = w.length;
    
            int[][] dp = new int[N + 1][W + 1];
    
            for (int i = 0; i < N; i++) {
                for (int j = 0; j <= W; j++) {
                    // dp[i+1][j]=i番目までの品物から重さの総和がj以下となるように選んだ場合の最大の価値総和
                    if (j < w[i]) {
                        dp[i + 1][j] = dp[i][j];
                    } else {
                        dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - w[i]] + v[i]);
                    }
                }
            }
    
            System.out.println(dp[N][W]); // 10
        }
    }

計算量は`O(NW)`

### 品物を何個でも選べる場合

ぱっと思いつくのは

    dp[i + 1][j] = max(dp[i][j - k * wi] + k * vi | k >= 0)

となるもの。プログラムを書くと


    public class Knapsack2 {
    
        public static void main(String[] args) {
            int[] w = { 3, 4, 2 };
            int[] v = { 4, 5, 3 };
            int W = 7;
            int N = w.length;
    
            int[][] dp = new int[N + 1][W + 1];
    
            for (int i = 0; i < N; i++) {
                for (int j = 0; j <= W; j++) {
                    int max = dp[i][j];
                    for (int k = 1; j >= k * w[i]; k++) {
                        max = Math.max(max, dp[i][j - k * w[i]] + k * v[i]);
                    }
                    dp[i + 1][j] = max;
                }
            }
    
            System.out.println(dp[N][W]); // 10
        }
    }

3重ループがあるので、計算量は`O(NW^2)`になってしまう。

ループが発生しないように考え直してみると、実は`dp[i + 1][j - wi] + vi`っていうのは`i`番目の品物を`k-1`個使った場合の結果に対してさらにもう1個足すという意味が含まれているので、

    dp[i + 1][j] = max(dp[i][j - k * wi] + k * vi | k >= 0)
                 = max(dp[i][j], dp[i + 1][j - wi] + vi)

と変形できる。これを使ってプログラムを書きなおすと

    public class Knapsack2 {
    
        public static void main(String[] args) {
            int[] w = { 3, 4, 2 };
            int[] v = { 4, 5, 3 };
            int W = 7;
            int N = w.length;
    
            int[][] dp = new int[N + 1][W + 1];
    
            for (int i = 0; i < N; i++) {
                for (int j = 0; j <= W; j++) {
                    if (j < w[i]) {
                        dp[i + 1][j] = dp[i][j];
                    } else {
                        dp[i + 1][j] = Math.max(dp[i][j], dp[i + 1][j - w[i]] + v[i]);
                    }
                }
            }
    
            System.out.println(dp[N][W]); // 10
        }
    }

これで計算量は`O(NW)`となる。

最初のパターンとほんのちょっと違うだけ。

### サイズが大きい場合

最初のパターンの制約を

> `1<=N<=100, 1<=wi<=10^7,1<=vi<=100, 1<=W<=10^7`

に変えた場合、`O(NW)`では厳しい。発想の転換が必要。

今まで重さに対する最大の価値をDPで計算していたが、今回は(サイズの制約がゆるい)**価値に対する最小の重みをDPで計算する**こととする。最終的にはDPの結果が`W`を超えない最大の価値を出力するようにすれば良い。

`i`番目までの品物から価値の総和が`j`以下となるように選んだ場合の重さの総和の最小値を`dp[i + 1][j]`としたとき、

    dp[i + 1][j] = dp[i][j] (j < vi)
                   min(dp[i][j], dp[i][j - vi] + wi)

が成り立つ。プログラムで書くと


    import java.util.Arrays;
    
    public class Knapsack3 {
    
        public static void main(String[] args) {
            int[] w = { 2, 1, 3, 2 };
            int[] v = { 3, 2, 4, 2 };
            int W = 5;
            int N = w.length;
            int V = 4; // viの取りうる最大値
    
            int[][] dp = new int[N + 1][W * V + 1];
            Arrays.fill(dp[0], Integer.MAX_VALUE); // 解が存在しない場合はInteger.MAX_VALUEに
            dp[0][0] = 0;
    
            for (int i = 0; i < N; i++) {
                for (int j = 0; j <= N * V; j++) {
                    // dp[i+1][j]=i番目までの品物から価値の総和がj以下となるように選んだ場合の重さの総和の最小値
                    if (j < v[i]) {
                        dp[i + 1][j] = dp[i][j];
                    } else {
                        // i-1番目までの品物から価値の総和がjとなるように選ぶ
                        // またはi-1番目の品物から価値の総和がj-viとなるように選び、i番目の品物を加える
                        dp[i + 1][j] = (int) Math.min((long) dp[i][j],
                                (long) dp[i][j - v[i]] + w[i]); // オーバーフロー対策
                    }
                }
            }
    
            int max = 0;
            for (int j = 0; j <= N * V; j++) {
                if (dp[N][j] <= W) {
                    max = j; // dp[N][j]がWを超えない最大のjが解
                }
            }
            System.out.println(max); // 7
        }
    }

計算量はO(NΣvi)で済む。

### 選ぶ品物の個数に制約がある場合

続く。
