Skip to content

14.4   0-1 Knapsack problem

The knapsack problem is an excellent introductory problem for dynamic programming and is the most common type of problem in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem, etc.

In this section, we will first solve the most common 0-1 knapsack problem.

Question

Given \(n\) items, the weight of the \(i\)-th item is \(wgt[i-1]\) and its value is \(val[i-1]\), and a knapsack with a capacity of \(cap\). Each item can be chosen only once. What is the maximum value of items that can be placed in the knapsack under the capacity limit?

Observe Figure 14-17, since the item number \(i\) starts counting from 1, and the array index starts from 0, thus the weight of item \(i\) corresponds to \(wgt[i-1]\) and the value corresponds to \(val[i-1]\).

Example data of the 0-1 knapsack

Figure 14-17   Example data of the 0-1 knapsack

We can consider the 0-1 knapsack problem as a process consisting of \(n\) rounds of decisions, where for each item there are two decisions: not to put it in or to put it in, thus the problem fits the decision tree model.

The objective of this problem is to "maximize the value of the items that can be put in the knapsack under the limited capacity," thus it is more likely a dynamic programming problem.

First step: Think about each round of decisions, define states, thereby obtaining the \(dp\) table

For each item, if not put into the knapsack, the capacity remains unchanged; if put in, the capacity is reduced. From this, the state definition can be obtained: the current item number \(i\) and knapsack capacity \(c\), denoted as \([i, c]\).

State \([i, c]\) corresponds to the sub-problem: the maximum value of the first \(i\) items in a knapsack of capacity \(c\), denoted as \(dp[i, c]\).

The solution we are looking for is \(dp[n, cap]\), so we need a two-dimensional \(dp\) table of size \((n+1) \times (cap+1)\).

Second step: Identify the optimal substructure, then derive the state transition equation

After making the decision for item \(i\), what remains is the sub-problem of decisions for the first \(i-1\) items, which can be divided into two cases.

  • Not putting item \(i\): The knapsack capacity remains unchanged, state changes to \([i-1, c]\).
  • Putting item \(i\): The knapsack capacity decreases by \(wgt[i-1]\), and the value increases by \(val[i-1]\), state changes to \([i-1, c-wgt[i-1]]\).

The above analysis reveals the optimal substructure of this problem: the maximum value \(dp[i, c]\) is equal to the larger value of the two schemes of not putting item \(i\) and putting item \(i\). From this, the state transition equation can be derived:

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

It is important to note that if the current item's weight \(wgt[i - 1]\) exceeds the remaining knapsack capacity \(c\), then the only option is not to put it in the knapsack.

Third step: Determine the boundary conditions and the order of state transitions

When there are no items or the knapsack capacity is \(0\), the maximum value is \(0\), i.e., the first column \(dp[i, 0]\) and the first row \(dp[0, c]\) are both equal to \(0\).

The current state \([i, c]\) transitions from the state directly above \([i-1, c]\) and the state to the upper left \([i-1, c-wgt[i-1]]\), thus, the entire \(dp\) table is traversed in order through two layers of loops.

Following the above analysis, we will next implement the solutions in the order of brute force search, memoized search, and dynamic programming.

The search code includes the following elements.

  • Recursive parameters: State \([i, c]\).
  • Return value: Solution to the sub-problem \(dp[i, c]\).
  • Termination condition: When the item number is out of bounds \(i = 0\) or the remaining capacity of the knapsack is \(0\), terminate the recursion and return the value \(0\).
  • Pruning: If the current item's weight exceeds the remaining capacity of the knapsack, the only option is not to put it in the knapsack.
knapsack.py
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]
    # 返回两种方案中价值更大的那一个
    return max(no, yes)
knapsack.cpp
/* 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];
    // 返回两种方案中价值更大的那一个
    return max(no, yes);
}
knapsack.java
/* 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];
    // 返回两种方案中价值更大的那一个
    return Math.max(no, yes);
}
knapsack.cs
/* 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];
    // 返回两种方案中价值更大的那一个
    return Math.Max(no, yes);
}
knapsack.go
/* 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]
    // 返回两种方案中价值更大的那一个
    return int(math.Max(float64(no), float64(yes)))
}
knapsack.swift
/* 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]
    // 返回两种方案中价值更大的那一个
    return max(no, yes)
}
knapsack.js
/* 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];
    // 返回两种方案中价值更大的那一个
    return Math.max(no, yes);
}
knapsack.ts
/* 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];
    // 返回两种方案中价值更大的那一个
    return Math.max(no, yes);
}
knapsack.dart
/* 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];
  // 返回两种方案中价值更大的那一个
  return max(no, yes);
}
knapsack.rs
/* 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];
    // 返回两种方案中价值更大的那一个
    std::cmp::max(no, yes)
}
knapsack.c
/* 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];
    // 返回两种方案中价值更大的那一个
    return myMax(no, yes);
}
knapsack.kt
/* 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]
    // 返回两种方案中价值更大的那一个
    return max(no, yes)
}
knapsack.rb
[class]{}-[func]{knapsack_dfs}
knapsack.zig
// 0-1 背包:暴力搜索
fn knapsackDFS(wgt: []i32, val: []i32, i: usize, c: usize) i32 {
    // 若已选完所有物品或背包无剩余容量,则返回价值 0
    if (i == 0 or c == 0) {
        return 0;
    }
    // 若超过背包容量,则只能选择不放入背包
    if (wgt[i - 1] > c) {
        return knapsackDFS(wgt, val, i - 1, c);
    }
    // 计算不放入和放入物品 i 的最大价值
    var no = knapsackDFS(wgt, val, i - 1, c);
    var yes = knapsackDFS(wgt, val, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1];
    // 返回两种方案中价值更大的那一个
    return @max(no, yes);
}
Code Visualization

As shown in Figure 14-18, since each item generates two search branches of not selecting and selecting, the time complexity is \(O(2^n)\).

Observing the recursive tree, it is easy to see that there are overlapping sub-problems, such as \(dp[1, 10]\), etc. When there are many items and the knapsack capacity is large, especially when there are many items of the same weight, the number of overlapping sub-problems will increase significantly.

The brute force search recursive tree of the 0-1 knapsack problem

Figure 14-18   The brute force search recursive tree of the 0-1 knapsack problem

To ensure that overlapping sub-problems are only calculated once, we use a memoization list mem to record the solutions to sub-problems, where mem[i][c] corresponds to \(dp[i, c]\).

After introducing memoization, the time complexity depends on the number of sub-problems, which is \(O(n \times cap)\). The implementation code is as follows:

knapsack.py
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]
    # 记录并返回两种方案中价值更大的那一个
    mem[i][c] = max(no, yes)
    return mem[i][c]
knapsack.cpp
/* 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];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = max(no, yes);
    return mem[i][c];
}
knapsack.java
/* 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];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = Math.max(no, yes);
    return mem[i][c];
}
knapsack.cs
/* 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];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = Math.Max(no, yes);
    return mem[i][c];
}
knapsack.go
/* 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]
    // 返回两种方案中价值更大的那一个
    mem[i][c] = int(math.Max(float64(no), float64(yes)))
    return mem[i][c]
}
knapsack.swift
/* 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]
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = max(no, yes)
    return mem[i][c]
}
knapsack.js
/* 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];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = Math.max(no, yes);
    return mem[i][c];
}
knapsack.ts
/* 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];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = Math.max(no, yes);
    return mem[i][c];
}
knapsack.dart
/* 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];
  // 记录并返回两种方案中价值更大的那一个
  mem[i][c] = max(no, yes);
  return mem[i][c];
}
knapsack.rs
/* 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];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = std::cmp::max(no, yes);
    mem[i][c]
}
knapsack.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];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = myMax(no, yes);
    return mem[i][c];
}
knapsack.kt
/* 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]
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = max(no, yes)
    return mem[i][c]
}
knapsack.rb
[class]{}-[func]{knapsack_dfs_mem}
knapsack.zig
// 0-1 背包:记忆化搜索
fn knapsackDFSMem(wgt: []i32, val: []i32, mem: anytype, i: usize, c: usize) i32 {
    // 若已选完所有物品或背包无剩余容量,则返回价值 0
    if (i == 0 or 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 的最大价值
    var no = knapsackDFSMem(wgt, val, mem, i - 1, c);
    var yes = knapsackDFSMem(wgt, val, mem, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1];
    // 记录并返回两种方案中价值更大的那一个
    mem[i][c] = @max(no, yes);
    return mem[i][c];
}
Code Visualization

Figure 14-19 shows the search branches that are pruned in memoized search.

The memoized search recursive tree of the 0-1 knapsack problem

Figure 14-19   The memoized search recursive tree of the 0-1 knapsack problem

3.   Method three: Dynamic programming

Dynamic programming essentially involves filling the \(dp\) table during the state transition, the code is shown in Figure 14-20:

knapsack.py
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]
knapsack.cpp
/* 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];
}
knapsack.java
/* 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];
}
knapsack.cs
/* 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];
}
knapsack.go
/* 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]
}
knapsack.swift
/* 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]
}
knapsack.js
/* 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];
}
knapsack.ts
/* 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];
}
knapsack.dart
/* 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];
}
knapsack.rs
/* 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]
}
knapsack.c
/* 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;
}
knapsack.kt
/* 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]
}
knapsack.rb
[class]{}-[func]{knapsack_dp}
knapsack.zig
// 0-1 背包:动态规划
fn knapsackDP(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 {
    comptime var n = wgt.len;
    // 初始化 dp 表
    var dp = [_][cap + 1]i32{[_]i32{0} ** (cap + 1)} ** (n + 1);
    // 状态转移
    for (1..n + 1) |i| {
        for (1..cap + 1) |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 - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]);
            }
        }
    }
    return dp[n][cap];
}
Code Visualization

As shown in the figures below, both the time complexity and space complexity are determined by the size of the array dp, i.e., \(O(n \times cap)\).

The dynamic programming process of the 0-1 knapsack problem

knapsack_dp_step2

knapsack_dp_step3

knapsack_dp_step4

knapsack_dp_step5

knapsack_dp_step6

knapsack_dp_step7

knapsack_dp_step8

knapsack_dp_step9

knapsack_dp_step10

knapsack_dp_step11

knapsack_dp_step12

knapsack_dp_step13

knapsack_dp_step14

Figure 14-20   The dynamic programming process of the 0-1 knapsack problem

4.   Space optimization

Since each state is only related to the state in the row above it, we can use two arrays to roll forward, reducing the space complexity from \(O(n^2)\) to \(O(n)\).

Further thinking, can we use just one array to achieve space optimization? It can be observed that each state is transferred from the cell directly above or from the upper left cell. If there is only one array, when starting to traverse the \(i\)-th row, that array still stores the state of row \(i-1\).

  • If using normal order traversal, then when traversing to \(dp[i, j]\), the values from the upper left \(dp[i-1, 1]\) ~ \(dp[i-1, j-1]\) may have already been overwritten, thus the correct state transition result cannot be obtained.
  • If using reverse order traversal, there will be no overwriting problem, and the state transition can be conducted correctly.

The figures below show the transition process from row \(i = 1\) to row \(i = 2\) in a single array. Please think about the differences between normal order traversal and reverse order traversal.

The space-optimized dynamic programming process of the 0-1 knapsack

knapsack_dp_comp_step2

knapsack_dp_comp_step3

knapsack_dp_comp_step4

knapsack_dp_comp_step5

knapsack_dp_comp_step6

Figure 14-21   The space-optimized dynamic programming process of the 0-1 knapsack

In the code implementation, we only need to delete the first dimension \(i\) of the array dp and change the inner loop to reverse traversal:

knapsack.py
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]
knapsack.cpp
/* 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];
}
knapsack.java
/* 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];
}
knapsack.cs
/* 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];
}
knapsack.go
/* 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]
}
knapsack.swift
/* 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]
}
knapsack.js
/* 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];
}
knapsack.ts
/* 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];
}
knapsack.dart
/* 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];
}
knapsack.rs
/* 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]
}
knapsack.c
/* 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;
}
knapsack.kt
/* 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]
}
knapsack.rb
[class]{}-[func]{knapsack_dp_comp}
knapsack.zig
// 0-1 背包:空间优化后的动态规划
fn knapsackDPComp(wgt: []i32, val: []i32, comptime cap: usize) i32 {
    var n = wgt.len;
    // 初始化 dp 表
    var dp = [_]i32{0} ** (cap + 1);
    // 状态转移
    for (1..n + 1) |i| {
        // 倒序遍历
        var c = cap;
        while (c > 0) : (c -= 1) {
            if (wgt[i - 1] < c) {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}
Code Visualization

Feel free to drop your insights, questions or suggestions