Files
hello-algo/ja/docs/chapter_dynamic_programming/knapsack_problem.md
T
Yudong Jin 7257641e0e Retranslate the complete Japanese online edition from Chinese (#2008)
* Retranslate the complete Japanese online edition from Chinese

* Fit Japanese homepage labels and localize heading permalinks

* Point Japanese contribution instructions to the Japanese preview

* Replace locale conditionals with multilingual UI tables
2026-10-30 02:12:26 +08:00

9.2 KiB

0-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] に対応します。

0-1 ナップサック問題のデータ例

0-1 ナップサック問題は、n 回の選択からなる過程と捉えられます。各品物について、入れないか入れるかの二つを選ぶため、決定木モデルに当てはまります。

目標が「容量の制限内で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いと考えられます。

第 1 段階:各回の選択を考えて状態を定義し、dp テーブルを作る

各品物を入れなければ容量は変わらず、入れれば容量が減ります。そこで状態を、現在の品物の番号 i とナップサックの容量 c からなる [i, c] と定義します。

状態 [i, c] に対応する部分問題は、最初の i 個の品物を使い、容量 c のナップサックで得られる最大価値で、その解を dp[i, c] とします。

求めるのは dp[n, cap] なので、(n+1) \times (cap+1) の二次元の dp テーブルが必要です。

第 2 段階:最適部分構造を見つけ、状態遷移式を導く

品物 i をどうするか決めると、残るのは最初の i-1 個に関する部分問題です。次の二つの場合に分かれます。

  • 品物 i を入れない:容量は変わらず、状態は [i-1, c] になります。
  • 品物 i を入れる:容量は wgt[i-1] 減り、価値は val[i-1] 増え、状態は [i-1, c-wgt[i-1]] になります。

ここから最適部分構造がわかります。最大価値 dp[i, c] は、品物 i を入れない場合と品物 i を入れる場合のうち、大きい方の価値です。したがって、状態遷移式は次のようになります。


dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])

現在の品物の重さ 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]] から遷移するため、二重ループで dp テーブル全体を順方向に走査すれば十分です。

以上の分析に基づき、全探索、メモ化探索、動的計画法を順に実装します。

方法 1:全探索

探索のコードには、次の要素があります。

  • 再帰の引数:状態 [i, c] です。
  • 戻り値:部分問題の解 dp[i, c] です。
  • 終了条件:品物の番号が範囲外の i = 0 になった場合、または残り容量が 0 になった場合は、再帰を終了して価値 0 を返します。
  • 枝刈り:現在の品物の重さが残り容量を超える場合は、入れない方だけを選びます。
[file]{knapsack}-[class]{}-[func]{knapsack_dfs}

下図のように、各品物から「選ばない」「選ぶ」の二つの探索分岐が生じるため、時間計算量は O(2^n) です。

再帰木には、dp[1, 10] などの重複した部分問題があります。品物が多く、容量が大きく、特に同じ重さの品物が多いと、重複する部分問題の数は大幅に増えます。

0-1 ナップサック問題の全探索の再帰木

方法 2:メモ化探索

重複した部分問題を 1 回だけ計算するため、記録用リスト mem に解を保存します。mem[i][c] は dp[i, c] に対応します。

メモ化すると、時間計算量は部分問題の数で決まり、O(n \times cap) になります。実装は次のとおりです。

[file]{knapsack}-[class]{}-[func]{knapsack_dfs_mem}

下図に、メモ化探索で枝刈りされる探索分岐を示します。

0-1 ナップサック問題のメモ化探索の再帰木

方法 3:動的計画法

動的計画法は、本質的には状態遷移によって dp テーブルを埋める過程です。コードを次に示します。

[file]{knapsack}-[class]{}-[func]{knapsack_dp}

下図のように、時間計算量と空間計算量は、どちらも配列 dp の大きさで決まり、O(n \times cap) です。

=== "<1>" 0-1 ナップサック問題を動的計画法で解く流れ

=== "<2>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 2

=== "<3>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 3

=== "<4>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 4

=== "<5>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 5

=== "<6>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 6

=== "<7>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 7

=== "<8>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 8

=== "<9>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 9

=== "<10>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 10

=== "<11>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 11

=== "<12>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 12

=== "<13>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 13

=== "<14>" 0-1 ナップサック問題を動的計画法で解く流れ:手順 14

空間の最適化

各状態は一つ前の行の状態だけに依存するため、二つの配列を交互に使えば、空間計算量を O(n \times cap) から O(cap) に下げられます。

さらに、配列を一つだけにできるでしょうか。各状態は真上か左上のマスから遷移します。配列が一つなら、i 行目の走査を始める時点では、まだ i-1 行目の状態が入っています。

  • 順方向に走査すると、dp[i, j] を計算する時点で、左上の dp[i-1, 1] ~ dp[i-1, j-1] がすでに上書きされている可能性があり、正しく状態遷移できません。
  • 逆方向に走査すれば、必要な値は上書きされず、正しく状態遷移できます。

下図に、配列一つで i = 1 行目から i = 2 行目へ移る過程を示します。順方向と逆方向の違いを考えてみてください。

=== "<1>" 空間最適化後の 0-1 ナップサック問題の動的計画法

=== "<2>" 空間最適化後の 0-1 ナップサック問題の動的計画法:手順 2

=== "<3>" 空間最適化後の 0-1 ナップサック問題の動的計画法:手順 3

=== "<4>" 空間最適化後の 0-1 ナップサック問題の動的計画法:手順 4

=== "<5>" 空間最適化後の 0-1 ナップサック問題の動的計画法:手順 5

=== "<6>" 空間最適化後の 0-1 ナップサック問題の動的計画法:手順 6

実装では、配列 dp の第 1 次元 i を取り除き、内側のループを逆順にするだけです。

[file]{knapsack}-[class]{}-[func]{knapsack_dp_comp}