14.4 0-1 ナップサック問題¶
ナップサック問題は、動的計画法の入門として非常に適した問題であり、動的計画法で最もよく見られる問題形式の1つです。これには 0-1 ナップサック問題、完全ナップサック問題、多重ナップサック問題など、多くの派生があります。
本節では、まず最も一般的な 0-1 ナップサック問題を解いていきます。
Question
\(n\) 個の品物が与えられ、\(i\) 番目の品物の重さは \(wgt[i-1]\)、価値は \(val[i-1]\) であり、容量 \(cap\) のナップサックがあります。各品物は1回しか選べないとき、ナップサック容量の制約下で入れられる品物の最大価値を求めてください。
以下の図を見てみましょう。品物番号 \(i\) は \(1\) から始まり、配列のインデックスは \(0\) から始まるため、品物 \(i\) は重さ \(wgt[i-1]\)、価値 \(val[i-1]\) に対応します。

図 14-17 0-1 ナップサックのサンプルデータ
0-1 ナップサック問題は、\(n\) 回の意思決定からなる過程とみなせます。各品物について「入れない」「入れる」という2つの選択肢があるため、この問題は決定木モデルを満たします。
この問題の目的は「ナップサック容量の制約下で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いです。
ステップ1:各ラウンドの選択を考え、状態を定義して、\(dp\) テーブルを得る
各品物について、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。ここから状態を、現在の品物番号 \(i\) とナップサック容量 \(c\) として定義し、\([i, c]\) と表せます。
状態 \([i, c]\) に対応する部分問題は、先頭 \(i\) 個の品物を容量 \(c\) のナップサックに入れるときの最大価値 であり、これを \(dp[i, c]\) と記します。
求めるべきものは \(dp[n, cap]\) なので、サイズ \((n+1) \times (cap+1)\) の2次元 \(dp\) テーブルが必要です。
ステップ2:最適部分構造を見つけ、状態遷移方程式を導く
品物 \(i\) に対する選択を行った後に残るのは、先頭 \(i-1\) 個の品物に対する部分問題であり、次の2つのケースに分けられます。
- 品物 \(i\) を入れない :ナップサック容量は変わらず、状態は \([i-1, c]\) に変化します。
- 品物 \(i\) を入れる :ナップサック容量は \(wgt[i-1]\) だけ減少し、価値は \(val[i-1]\) だけ増加して、状態は \([i-1, c-wgt[i-1]]\) に変化します。
以上の分析から、この問題の最適部分構造が分かります。すなわち、最大価値 \(dp[i, c]\) は、品物 \(i\) を入れない場合と入れる場合のうち、より価値の大きい方に等しい ということです。これにより、次の状態遷移方程式を導けます。
注意すべき点として、現在の品物の重さ \(wgt[i - 1]\) が残りのナップサック容量 \(c\) を超える場合は、入れない選択しかできません。
ステップ3:境界条件と状態遷移の順序を決める
品物がない場合、またはナップサック容量が \(0\) の場合、最大価値は \(0\) です。すなわち、先頭列 \(dp[i, 0]\) と先頭行 \(dp[0, c]\) はいずれも \(0\) になります。
現在の状態 \([i, c]\) は、上側の状態 \([i-1, c]\) と左上の状態 \([i-1, c-wgt[i-1]]\) から遷移してくるため、2重ループで \(dp\) テーブル全体を順方向に走査すれば十分です。
以上の分析に基づき、次に全探索、メモ化探索、動的計画法の順で実装していきます。
1. 方法1:全探索¶
探索コードには次の要素が含まれます。
- 再帰引数:状態 \([i, c]\) です。
- 戻り値:部分問題の解 \(dp[i, c]\) です。
- 終了条件:品物番号が範囲外である \(i = 0\)、またはナップサックの残り容量が \(0\) のとき、再帰を終了して価値 \(0\) を返します。
- 枝刈り:現在の品物の重さがナップサックの残り容量を超える場合、入れない選択しかできません。
def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:
"""0-1 ナップサック:総当たり探索"""
# すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 or c == 0:
return 0
# ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i - 1] > c:
return knapsack_dfs(wgt, val, i - 1, c)
# 品物 i を入れない場合と入れる場合の最大価値を計算する
no = knapsack_dfs(wgt, val, i - 1, c)
yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]
# 2つの案のうち価値が大きいほうを返す
return max(no, yes)
/* 0-1 ナップサック:総当たり探索 */
int knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFS(wgt, val, i - 1, c);
int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
return max(no, yes);
}
/* 0-1 ナップサック:総当たり探索 */
int knapsackDFS(int[] wgt, int[] val, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFS(wgt, val, i - 1, c);
int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
return Math.max(no, yes);
}
/* 0-1 ナップサック:総当たり探索 */
int KnapsackDFS(int[] weight, int[] val, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (weight[i - 1] > c) {
return KnapsackDFS(weight, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = KnapsackDFS(weight, val, i - 1, c);
int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
return Math.Max(no, yes);
}
/* 0-1 ナップサック:総当たり探索 */
func knapsackDFS(wgt, val []int, i, c int) int {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 || c == 0 {
return 0
}
// ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i-1] > c {
return knapsackDFS(wgt, val, i-1, c)
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
no := knapsackDFS(wgt, val, i-1, c)
yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]
// 2つの案のうち価値が大きいほうを返す
return int(math.Max(float64(no), float64(yes)))
}
/* 0-1 ナップサック:総当たり探索 */
func knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 || c == 0 {
return 0
}
// ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i - 1] > c {
return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)
let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]
// 2つの案のうち価値が大きいほうを返す
return max(no, yes)
}
/* 0-1 ナップサック:総当たり探索 */
function knapsackDFS(wgt, val, i, c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i === 0 || c === 0) {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
const no = knapsackDFS(wgt, val, i - 1, c);
const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
return Math.max(no, yes);
}
/* 0-1 ナップサック:総当たり探索 */
function knapsackDFS(
wgt: Array<number>,
val: Array<number>,
i: number,
c: number
): number {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i === 0 || c === 0) {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
const no = knapsackDFS(wgt, val, i - 1, c);
const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
return Math.max(no, yes);
}
/* 0-1 ナップサック:総当たり探索 */
int knapsackDFS(List<int> wgt, List<int> val, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFS(wgt, val, i - 1, c);
int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
return max(no, yes);
}
/* 0-1 ナップサック:総当たり探索 */
fn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 || c == 0 {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i - 1] > c as i32 {
return knapsack_dfs(wgt, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
let no = knapsack_dfs(wgt, val, i - 1, c);
let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
std::cmp::max(no, yes)
}
/* 0-1 ナップサック:総当たり探索 */
int knapsackDFS(int wgt[], int val[], int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFS(wgt, val, i - 1, c);
int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2つの案のうち価値が大きいほうを返す
return myMax(no, yes);
}
/* 0-1 ナップサック:総当たり探索 */
fun knapsackDFS(
wgt: IntArray,
_val: IntArray,
i: Int,
c: Int
): Int {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, _val, i - 1, c)
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
val no = knapsackDFS(wgt, _val, i - 1, c)
val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]
// 2つの案のうち価値が大きいほうを返す
return max(no, yes)
}
### 0-1 ナップサック: 全探索 ###
def knapsack_dfs(wgt, val, i, c)
# すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
return 0 if i == 0 || c == 0
# ナップサック容量を超える場合は、入れない選択しかできない
return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c
# 品物 i を入れない場合と入れる場合の最大価値を計算する
no = knapsack_dfs(wgt, val, i - 1, c)
yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]
# 2つの案のうち価値が大きいほうを返す
[no, yes].max
end
コードの可視化
以下の図のように、各品物ごとに「選ばない」「選ぶ」の2つの探索分岐が生じるため、時間計算量は \(O(2^n)\) です。
再帰木を観察すると、\(dp[1, 10]\) などの重複部分問題が存在することが分かります。品物数が多く、ナップサック容量が大きく、特に同じ重さの品物が多い場合には、重複部分問題の数は大幅に増加します。

図 14-18 0-1 ナップサック問題の全探索の再帰木
2. 方法2:メモ化探索¶
重複部分問題が一度だけ計算されるようにするため、メモ配列 mem を用いて部分問題の解を記録します。ここで mem[i][c] は \(dp[i, c]\) に対応します。
メモ化を導入すると、時間計算量は部分問題の数に依存し、すなわち \(O(n \times cap)\) になります。実装コードは次のとおりです。
def knapsack_dfs_mem(
wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int
) -> int:
"""0-1 ナップサック:メモ化探索"""
# すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 or c == 0:
return 0
# 既に記録があればそのまま返す
if mem[i][c] != -1:
return mem[i][c]
# ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i - 1] > c:
return knapsack_dfs_mem(wgt, val, mem, i - 1, c)
# 品物 i を入れない場合と入れる場合の最大価値を計算する
no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)
yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]
# 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = max(no, yes)
return mem[i][c]
/* 0-1 ナップサック:メモ化探索 */
int knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// 既に記録があればそのまま返す
if (mem[i][c] != -1) {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFSMem(wgt, val, mem, i - 1, c);
int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = max(no, yes);
return mem[i][c];
}
/* 0-1 ナップサック:メモ化探索 */
int knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// 既に記録があればそのまま返す
if (mem[i][c] != -1) {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFSMem(wgt, val, mem, i - 1, c);
int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = Math.max(no, yes);
return mem[i][c];
}
/* 0-1 ナップサック:メモ化探索 */
int KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// 既に記録があればそのまま返す
if (mem[i][c] != -1) {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (weight[i - 1] > c) {
return KnapsackDFSMem(weight, val, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = KnapsackDFSMem(weight, val, mem, i - 1, c);
int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = Math.Max(no, yes);
return mem[i][c];
}
/* 0-1 ナップサック:メモ化探索 */
func knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 || c == 0 {
return 0
}
// 既に記録があればそのまま返す
if mem[i][c] != -1 {
return mem[i][c]
}
// ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i-1] > c {
return knapsackDFSMem(wgt, val, mem, i-1, c)
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
no := knapsackDFSMem(wgt, val, mem, i-1, c)
yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]
// 2つの案のうち価値が大きいほうを返す
mem[i][c] = int(math.Max(float64(no), float64(yes)))
return mem[i][c]
}
/* 0-1 ナップサック:メモ化探索 */
func knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 || c == 0 {
return 0
}
// 既に記録があればそのまま返す
if mem[i][c] != -1 {
return mem[i][c]
}
// ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i - 1] > c {
return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)
let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = max(no, yes)
return mem[i][c]
}
/* 0-1 ナップサック:メモ化探索 */
function knapsackDFSMem(wgt, val, mem, i, c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i === 0 || c === 0) {
return 0;
}
// 既に記録があればそのまま返す
if (mem[i][c] !== -1) {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
const no = knapsackDFSMem(wgt, val, mem, i - 1, c);
const yes =
knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = Math.max(no, yes);
return mem[i][c];
}
/* 0-1 ナップサック:メモ化探索 */
function knapsackDFSMem(
wgt: Array<number>,
val: Array<number>,
mem: Array<Array<number>>,
i: number,
c: number
): number {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i === 0 || c === 0) {
return 0;
}
// 既に記録があればそのまま返す
if (mem[i][c] !== -1) {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
const no = knapsackDFSMem(wgt, val, mem, i - 1, c);
const yes =
knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = Math.max(no, yes);
return mem[i][c];
}
/* 0-1 ナップサック:メモ化探索 */
int knapsackDFSMem(
List<int> wgt,
List<int> val,
List<List<int>> mem,
int i,
int c,
) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// 既に記録があればそのまま返す
if (mem[i][c] != -1) {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFSMem(wgt, val, mem, i - 1, c);
int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = max(no, yes);
return mem[i][c];
}
/* 0-1 ナップサック:メモ化探索 */
fn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if i == 0 || c == 0 {
return 0;
}
// 既に記録があればそのまま返す
if mem[i][c] != -1 {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if wgt[i - 1] > c as i32 {
return knapsack_dfs_mem(wgt, val, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);
let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = std::cmp::max(no, yes);
mem[i][c]
}
/* 0-1 ナップサック:メモ化探索 */
int knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0;
}
// 既に記録があればそのまま返す
if (mem[i][c] != -1) {
return mem[i][c];
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);
int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = myMax(no, yes);
return mem[i][c];
}
/* 0-1 ナップサック:メモ化探索 */
fun knapsackDFSMem(
wgt: IntArray,
_val: IntArray,
mem: Array<IntArray>,
i: Int,
c: Int
): Int {
// すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
if (i == 0 || c == 0) {
return 0
}
// 既に記録があればそのまま返す
if (mem[i][c] != -1) {
return mem[i][c]
}
// ナップサック容量を超える場合は、入れない選択しかできない
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, _val, mem, i - 1, c)
}
// 品物 i を入れない場合と入れる場合の最大価値を計算する
val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)
val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]
// 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = max(no, yes)
return mem[i][c]
}
### 0-1 ナップサック: メモ化探索 ###
def knapsack_dfs_mem(wgt, val, mem, i, c)
# すべての品物を選び終えたか、ナップサックに残り容量がなければ、価値 0 を返す
return 0 if i == 0 || c == 0
# 既に記録があればそのまま返す
return mem[i][c] if mem[i][c] != -1
# ナップサック容量を超える場合は、入れない選択しかできない
return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c
# 品物 i を入れない場合と入れる場合の最大価値を計算する
no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)
yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]
# 2 つの案のうち価値が大きい方を記録して返す
mem[i][c] = [no, yes].max
end
コードの可視化
次の図は、メモ化探索で剪定された探索分岐を示しています。

図 14-19 0-1 ナップサック問題のメモ化探索の再帰木
3. 方法3:動的計画法¶
動的計画法の本質は、状態遷移に従って \(dp\) テーブルを埋めていく過程です。コードは次のようになります。
def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:
"""0-1 ナップサック:動的計画法"""
n = len(wgt)
# dp テーブルを初期化
dp = [[0] * (cap + 1) for _ in range(n + 1)]
# 状態遷移
for i in range(1, n + 1):
for c in range(1, cap + 1):
if wgt[i - 1] > c:
# ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c]
else:
# 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])
return dp[n][cap]
/* 0-1 ナップサック:動的計画法 */
int knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// dp テーブルを初期化
vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));
// 状態遷移
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[n][cap];
}
/* 0-1 ナップサック:動的計画法 */
int knapsackDP(int[] wgt, int[] val, int cap) {
int n = wgt.length;
// dp テーブルを初期化
int[][] dp = new int[n + 1][cap + 1];
// 状態遷移
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[n][cap];
}
/* 0-1 ナップサック:動的計画法 */
int KnapsackDP(int[] weight, int[] val, int cap) {
int n = weight.Length;
// dp テーブルを初期化
int[,] dp = new int[n + 1, cap + 1];
// 状態遷移
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (weight[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i, c] = dp[i - 1, c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);
}
}
}
return dp[n, cap];
}
/* 0-1 ナップサック:動的計画法 */
func knapsackDP(wgt, val []int, cap int) int {
n := len(wgt)
// dp テーブルを初期化
dp := make([][]int, n+1)
for i := 0; i <= n; i++ {
dp[i] = make([]int, cap+1)
}
// 状態遷移
for i := 1; i <= n; i++ {
for c := 1; c <= cap; c++ {
if wgt[i-1] > c {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i-1][c]
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))
}
}
}
return dp[n][cap]
}
/* 0-1 ナップサック:動的計画法 */
func knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {
let n = wgt.count
// dp テーブルを初期化
var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)
// 状態遷移
for i in 1 ... n {
for c in 1 ... cap {
if wgt[i - 1] > c {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c]
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])
}
}
}
return dp[n][cap]
}
/* 0-1 ナップサック:動的計画法 */
function knapsackDP(wgt, val, cap) {
const n = wgt.length;
// dp テーブルを初期化
const dp = Array(n + 1)
.fill(0)
.map(() => Array(cap + 1).fill(0));
// 状態遷移
for (let i = 1; i <= n; i++) {
for (let c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = Math.max(
dp[i - 1][c],
dp[i - 1][c - wgt[i - 1]] + val[i - 1]
);
}
}
}
return dp[n][cap];
}
/* 0-1 ナップサック:動的計画法 */
function knapsackDP(
wgt: Array<number>,
val: Array<number>,
cap: number
): number {
const n = wgt.length;
// dp テーブルを初期化
const dp = Array.from({ length: n + 1 }, () =>
Array.from({ length: cap + 1 }, () => 0)
);
// 状態遷移
for (let i = 1; i <= n; i++) {
for (let c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = Math.max(
dp[i - 1][c],
dp[i - 1][c - wgt[i - 1]] + val[i - 1]
);
}
}
}
return dp[n][cap];
}
/* 0-1 ナップサック:動的計画法 */
int knapsackDP(List<int> wgt, List<int> val, int cap) {
int n = wgt.length;
// dp テーブルを初期化
List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));
// 状態遷移
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[n][cap];
}
/* 0-1 ナップサック:動的計画法 */
fn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {
let n = wgt.len();
// dp テーブルを初期化
let mut dp = vec![vec![0; cap + 1]; n + 1];
// 状態遷移
for i in 1..=n {
for c in 1..=cap {
if wgt[i - 1] > c as i32 {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = std::cmp::max(
dp[i - 1][c],
dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],
);
}
}
}
dp[n][cap]
}
/* 0-1 ナップサック:動的計画法 */
int knapsackDP(int wgt[], int val[], int cap, int wgtSize) {
int n = wgtSize;
// dp テーブルを初期化
int **dp = malloc((n + 1) * sizeof(int *));
for (int i = 0; i <= n; i++) {
dp[i] = calloc(cap + 1, sizeof(int));
}
// 状態遷移
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
}
int res = dp[n][cap];
// メモリを解放する
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
return res;
}
/* 0-1 ナップサック:動的計画法 */
fun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {
val n = wgt.size
// dp テーブルを初期化
val dp = Array(n + 1) { IntArray(cap + 1) }
// 状態遷移
for (i in 1..n) {
for (c in 1..cap) {
if (wgt[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c]
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])
}
}
}
return dp[n][cap]
}
### 0-1 ナップサック: 動的計画法 ###
def knapsack_dp(wgt, val, cap)
n = wgt.length
# dp テーブルを初期化
dp = Array.new(n + 1) { Array.new(cap + 1, 0) }
# 状態遷移
for i in 1...(n + 1)
for c in 1...(cap + 1)
if wgt[i - 1] > c
# ナップサック容量を超えるなら品物 i は選ばない
dp[i][c] = dp[i - 1][c]
else
# 品物 i を選ばない場合と選ぶ場合の大きい方
dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max
end
end
end
dp[n][cap]
end
コードの可視化
以下の図のように、時間計算量と空間計算量はいずれも配列 dp のサイズによって決まり、\(O(n \times cap)\) です。














図 14-20 0-1 ナップサック問題の動的計画法の過程
4. 空間最適化¶
各状態は直前の行の状態にしか依存しないため、2つの配列をローテーションして用いることで、空間計算量を \(O(n^2)\) から \(O(n)\) に削減できます。
さらに考えると、1つの配列だけで空間最適化を実現できるでしょうか。観察すると、各状態は真上または左上のマスから遷移してきます。配列が1つしかないと仮定すると、\(i\) 行目の走査を開始した時点では、その配列にはまだ \(i-1\) 行目の状態が格納されています。
- 順方向に走査すると、\(dp[i, j]\) に到達した時点で、左上にある \(dp[i-1, 1]\) ~ \(dp[i-1, j-1]\) の値がすでに上書きされている可能性があり、正しい状態遷移結果を得られません。
- 逆方向に走査すれば、上書きの問題は発生せず、状態遷移を正しく行えます。
次の図は、単一配列のもとで \(i = 1\) 行目から \(i = 2\) 行目へ変換する過程を示しています。順方向走査と逆方向走査の違いを考えてみてください。






図 14-21 0-1 ナップサックの空間最適化後の動的計画法の過程
コード実装では、配列 dp の第1次元 \(i\) をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。
def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:
"""0-1 ナップサック:空間最適化後の動的計画法"""
n = len(wgt)
# dp テーブルを初期化
dp = [0] * (cap + 1)
# 状態遷移
for i in range(1, n + 1):
# 逆順に走査する
for c in range(cap, 0, -1):
if wgt[i - 1] > c:
# ナップサック容量を超えるなら品物 i は選ばない
dp[c] = dp[c]
else:
# 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])
return dp[cap]
/* 0-1 ナップサック:空間最適化後の動的計画法 */
int knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// dp テーブルを初期化
vector<int> dp(cap + 1, 0);
// 状態遷移
for (int i = 1; i <= n; i++) {
// 逆順に走査する
for (int c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
int knapsackDPComp(int[] wgt, int[] val, int cap) {
int n = wgt.length;
// dp テーブルを初期化
int[] dp = new int[cap + 1];
// 状態遷移
for (int i = 1; i <= n; i++) {
// 逆順に走査する
for (int c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
int KnapsackDPComp(int[] weight, int[] val, int cap) {
int n = weight.Length;
// dp テーブルを初期化
int[] dp = new int[cap + 1];
// 状態遷移
for (int i = 1; i <= n; i++) {
// 逆順に走査する
for (int c = cap; c > 0; c--) {
if (weight[i - 1] > c) {
// ナップサック容量を超えるなら品物 i は選ばない
dp[c] = dp[c];
} else {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
func knapsackDPComp(wgt, val []int, cap int) int {
n := len(wgt)
// dp テーブルを初期化
dp := make([]int, cap+1)
// 状態遷移
for i := 1; i <= n; i++ {
// 逆順に走査する
for c := cap; c >= 1; c-- {
if wgt[i-1] <= c {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))
}
}
}
return dp[cap]
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
func knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {
let n = wgt.count
// dp テーブルを初期化
var dp = Array(repeating: 0, count: cap + 1)
// 状態遷移
for i in 1 ... n {
// 逆順に走査する
for c in (1 ... cap).reversed() {
if wgt[i - 1] <= c {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])
}
}
}
return dp[cap]
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
function knapsackDPComp(wgt, val, cap) {
const n = wgt.length;
// dp テーブルを初期化
const dp = Array(cap + 1).fill(0);
// 状態遷移
for (let i = 1; i <= n; i++) {
// 逆順に走査する
for (let c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
function knapsackDPComp(
wgt: Array<number>,
val: Array<number>,
cap: number
): number {
const n = wgt.length;
// dp テーブルを初期化
const dp = Array(cap + 1).fill(0);
// 状態遷移
for (let i = 1; i <= n; i++) {
// 逆順に走査する
for (let c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
int knapsackDPComp(List<int> wgt, List<int> val, int cap) {
int n = wgt.length;
// dp テーブルを初期化
List<int> dp = List.filled(cap + 1, 0);
// 状態遷移
for (int i = 1; i <= n; i++) {
// 逆順に走査する
for (int c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
fn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {
let n = wgt.len();
// dp テーブルを初期化
let mut dp = vec![0; cap + 1];
// 状態遷移
for i in 1..=n {
// 逆順に走査する
for c in (1..=cap).rev() {
if wgt[i - 1] <= c as i32 {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);
}
}
}
dp[cap]
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
int knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {
int n = wgtSize;
// dp テーブルを初期化
int *dp = calloc(cap + 1, sizeof(int));
// 状態遷移
for (int i = 1; i <= n; i++) {
// 逆順に走査する
for (int c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
int res = dp[cap];
// メモリを解放する
free(dp);
return res;
}
/* 0-1 ナップサック:空間最適化後の動的計画法 */
fun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {
val n = wgt.size
// dp テーブルを初期化
val dp = IntArray(cap + 1)
// 状態遷移
for (i in 1..n) {
// 逆順に走査する
for (c in cap downTo 1) {
if (wgt[i - 1] <= c) {
// 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])
}
}
}
return dp[cap]
}
### 0-1 ナップサック: 空間最適化後の動的計画法 ###
def knapsack_dp_comp(wgt, val, cap)
n = wgt.length
# dp テーブルを初期化
dp = Array.new(cap + 1, 0)
# 状態遷移
for i in 1...(n + 1)
# 逆順に走査する
for c in cap.downto(1)
if wgt[i - 1] > c
# ナップサック容量を超えるなら品物 i は選ばない
dp[c] = dp[c]
else
# 品物 i を選ばない場合と選ぶ場合の大きい方
dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max
end
end
end
dp[cap]
end