Skip to content

14.5   Unbounded knapsack problem

In this section, we first solve another common knapsack problem: the unbounded knapsack, and then explore a special case of it: the coin change problem.

14.5.1   Unbounded knapsack problem

Question

Given \(n\) items, where the weight of the \(i^{th}\) item is \(wgt[i-1]\) and its value is \(val[i-1]\), and a backpack with a capacity of \(cap\). Each item can be selected multiple times. What is the maximum value of the items that can be put into the backpack without exceeding its capacity? See the example below.

Example data for the unbounded knapsack problem

Figure 14-22   Example data for the unbounded knapsack problem

1.   Dynamic programming approach

The unbounded knapsack problem is very similar to the 0-1 knapsack problem, the only difference being that there is no limit on the number of times an item can be chosen.

  • In the 0-1 knapsack problem, there is only one of each item, so after placing item \(i\) into the backpack, you can only choose from the previous \(i-1\) items.
  • In the unbounded knapsack problem, the quantity of each item is unlimited, so after placing item \(i\) in the backpack, you can still choose from the previous \(i\) items.

Under the rules of the unbounded knapsack problem, the state \([i, c]\) can change in two ways.

  • Not putting item \(i\) in: As with the 0-1 knapsack problem, transition to \([i-1, c]\).
  • Putting item \(i\) in: Unlike the 0-1 knapsack problem, transition to \([i, c-wgt[i-1]]\).

The state transition equation thus becomes:

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

2.   Code implementation

Comparing the code for the two problems, the state transition changes from \(i-1\) to \(i\), the rest is completely identical:

unbounded_knapsack.py
def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:
    """完全背包:动态规划"""
    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][c - wgt[i - 1]] + val[i - 1])
    return dp[n][cap]
unbounded_knapsack.cpp
/* 完全背包:动态规划 */
int unboundedKnapsackDP(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][c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[n][cap];
}
unbounded_knapsack.java
/* 完全背包:动态规划 */
int unboundedKnapsackDP(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][c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[n][cap];
}
unbounded_knapsack.cs
/* 完全背包:动态规划 */
int UnboundedKnapsackDP(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, c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[n, cap];
}
unbounded_knapsack.go
/* 完全背包:动态规划 */
func unboundedKnapsackDP(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][c-wgt[i-1]]+val[i-1])))
            }
        }
    }
    return dp[n][cap]
}
unbounded_knapsack.swift
/* 完全背包:动态规划 */
func unboundedKnapsackDP(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][c - wgt[i - 1]] + val[i - 1])
            }
        }
    }
    return dp[n][cap]
}
unbounded_knapsack.js
/* 完全背包:动态规划 */
function unboundedKnapsackDP(wgt, val, cap) {
    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][c - wgt[i - 1]] + val[i - 1]
                );
            }
        }
    }
    return dp[n][cap];
}
unbounded_knapsack.ts
/* 完全背包:动态规划 */
function unboundedKnapsackDP(
    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][c - wgt[i - 1]] + val[i - 1]
                );
            }
        }
    }
    return dp[n][cap];
}
unbounded_knapsack.dart
/* 完全背包:动态规划 */
int unboundedKnapsackDP(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][c - wgt[i - 1]] + val[i - 1]);
      }
    }
  }
  return dp[n][cap];
}
unbounded_knapsack.rs
/* 完全背包:动态规划 */
fn unbounded_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][c - wgt[i - 1] as usize] + val[i - 1]);
            }
        }
    }
    return dp[n][cap];
}
unbounded_knapsack.c
/* 完全背包:动态规划 */
int unboundedKnapsackDP(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][c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    int res = dp[n][cap];
    // 释放内存
    for (int i = 0; i <= n; i++) {
        free(dp[i]);
    }
    return res;
}
unbounded_knapsack.kt
/* 完全背包:动态规划 */
fun unboundedKnapsackDP(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][c - wgt[i - 1]] + _val[i - 1])
            }
        }
    }
    return dp[n][cap]
}
unbounded_knapsack.rb
[class]{}-[func]{unbounded_knapsack_dp}
unbounded_knapsack.zig
// 完全背包:动态规划
fn unboundedKnapsackDP(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][c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]);
            }
        }
    }
    return dp[n][cap];
}
Code Visualization

3.   Space optimization

Since the current state comes from the state to the left and above, the space-optimized solution should perform a forward traversal for each row in the \(dp\) table.

This traversal order is the opposite of that for the 0-1 knapsack. Please refer to Figure 14-23 to understand the difference.

Dynamic programming process for the unbounded knapsack problem after space optimization

unbounded_knapsack_dp_comp_step2

unbounded_knapsack_dp_comp_step3

unbounded_knapsack_dp_comp_step4

unbounded_knapsack_dp_comp_step5

unbounded_knapsack_dp_comp_step6

Figure 14-23   Dynamic programming process for the unbounded knapsack problem after space optimization

The code implementation is quite simple, just remove the first dimension of the array dp:

unbounded_knapsack.py
def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:
    """完全背包:空间优化后的动态规划"""
    n = len(wgt)
    # 初始化 dp 表
    dp = [0] * (cap + 1)
    # 状态转移
    for i in range(1, n + 1):
        # 正序遍历
        for c in range(1, cap + 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]
unbounded_knapsack.cpp
/* 完全背包:空间优化后的动态规划 */
int unboundedKnapsackDPComp(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 = 1; c <= cap; c++) {
            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];
}
unbounded_knapsack.java
/* 完全背包:空间优化后的动态规划 */
int unboundedKnapsackDPComp(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 = 1; c <= cap; c++) {
            if (wgt[i - 1] > c) {
                // 若超过背包容量,则不选物品 i
                dp[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}
unbounded_knapsack.cs
/* 完全背包:空间优化后的动态规划 */
int UnboundedKnapsackDPComp(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 = 1; c <= cap; c++) {
            if (wgt[i - 1] > c) {
                // 若超过背包容量,则不选物品 i
                dp[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}
unbounded_knapsack.go
/* 完全背包:空间优化后的动态规划 */
func unboundedKnapsackDPComp(wgt, val []int, cap int) int {
    n := len(wgt)
    // 初始化 dp 表
    dp := make([]int, cap+1)
    // 状态转移
    for i := 1; i <= n; i++ {
        for c := 1; c <= cap; c++ {
            if wgt[i-1] > c {
                // 若超过背包容量,则不选物品 i
                dp[c] = dp[c]
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))
            }
        }
    }
    return dp[cap]
}
unbounded_knapsack.swift
/* 完全背包:空间优化后的动态规划 */
func unboundedKnapsackDPComp(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 {
            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]
}
unbounded_knapsack.js
/* 完全背包:状态压缩后的动态规划 */
function unboundedKnapsackDPComp(wgt, val, cap) {
    const n = wgt.length;
    // 初始化 dp 表
    const dp = 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[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}
unbounded_knapsack.ts
/* 完全背包:状态压缩后的动态规划 */
function unboundedKnapsackDPComp(
    wgt: Array<number>,
    val: Array<number>,
    cap: number
): number {
    const n = wgt.length;
    // 初始化 dp 表
    const dp = 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[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}
unbounded_knapsack.dart
/* 完全背包:空间优化后的动态规划 */
int unboundedKnapsackDPComp(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 = 1; c <= cap; c++) {
      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];
}
unbounded_knapsack.rs
/* 完全背包:空间优化后的动态规划 */
fn unbounded_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 {
            if wgt[i - 1] > c as i32 {
                // 若超过背包容量,则不选物品 i
                dp[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);
            }
        }
    }
    dp[cap]
}
unbounded_knapsack.c
/* 完全背包:空间优化后的动态规划 */
int unboundedKnapsackDPComp(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 = 1; c <= cap; c++) {
            if (wgt[i - 1] > c) {
                // 若超过背包容量,则不选物品 i
                dp[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
            }
        }
    }
    int res = dp[cap];
    // 释放内存
    free(dp);
    return res;
}
unbounded_knapsack.kt
/* 完全背包:空间优化后的动态规划 */
fun unboundedKnapsackDPComp(
    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 1..cap) {
            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]
}
unbounded_knapsack.rb
[class]{}-[func]{unbounded_knapsack_dp_comp}
unbounded_knapsack.zig
// 完全背包:空间优化后的动态规划
fn unboundedKnapsackDPComp(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 {
    comptime var n = wgt.len;
    // 初始化 dp 表
    var dp = [_]i32{0} ** (cap + 1);
    // 状态转移
    for (1..n + 1) |i| {
        for (1..cap + 1) |c| {
            if (wgt[i - 1] > c) {
                // 若超过背包容量,则不选物品 i
                dp[c] = dp[c];
            } else {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]);
            }
        }
    }
    return dp[cap];
}
Code Visualization

14.5.2   Coin change problem

The knapsack problem is a representative of a large class of dynamic programming problems and has many variants, such as the coin change problem.

Question

Given \(n\) types of coins, the denomination of the \(i^{th}\) type of coin is \(coins[i - 1]\), and the target amount is \(amt\). Each type of coin can be selected multiple times. What is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return \(-1\). See the example below.

Example data for the coin change problem

Figure 14-24   Example data for the coin change problem

1.   Dynamic programming approach

The coin change can be seen as a special case of the unbounded knapsack problem, sharing the following similarities and differences.

  • The two problems can be converted into each other: "item" corresponds to "coin", "item weight" corresponds to "coin denomination", and "backpack capacity" corresponds to "target amount".
  • The optimization goals are opposite: the unbounded knapsack problem aims to maximize the value of items, while the coin change problem aims to minimize the number of coins.
  • The unbounded knapsack problem seeks solutions "not exceeding" the backpack capacity, while the coin change seeks solutions that "exactly" make up the target amount.

First step: Think through each round's decision-making, define the state, and thus derive the \(dp\) table

The state \([i, a]\) corresponds to the sub-problem: the minimum number of coins that can make up the amount \(a\) using the first \(i\) types of coins, denoted as \(dp[i, a]\).

The two-dimensional \(dp\) table is of size \((n+1) \times (amt+1)\).

Second step: Identify the optimal substructure and derive the state transition equation

This problem differs from the unbounded knapsack problem in two aspects of the state transition equation.

  • This problem seeks the minimum, so the operator \(\max()\) needs to be changed to \(\min()\).
  • The optimization is focused on the number of coins, so simply add \(+1\) when a coin is chosen.
\[ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \]

Third step: Define boundary conditions and state transition order

When the target amount is \(0\), the minimum number of coins needed to make it up is \(0\), so all \(dp[i, 0]\) in the first column are \(0\).

When there are no coins, it is impossible to make up any amount >0, which is an invalid solution. To allow the \(\min()\) function in the state transition equation to recognize and filter out invalid solutions, consider using \(+\infty\) to represent them, i.e., set all \(dp[0, a]\) in the first row to \(+\infty\).

2.   Code implementation

Most programming languages do not provide a \(+\infty\) variable, only the maximum value of an integer int can be used as a substitute. This can lead to overflow: the \(+1\) operation in the state transition equation may overflow.

For this reason, we use the number \(amt + 1\) to represent an invalid solution, because the maximum number of coins needed to make up \(amt\) is at most \(amt\). Before returning the result, check if \(dp[n, amt]\) equals \(amt + 1\), and if so, return \(-1\), indicating that the target amount cannot be made up. The code is as follows:

coin_change.py
def coin_change_dp(coins: list[int], amt: int) -> int:
    """零钱兑换:动态规划"""
    n = len(coins)
    MAX = amt + 1
    # 初始化 dp 表
    dp = [[0] * (amt + 1) for _ in range(n + 1)]
    # 状态转移:首行首列
    for a in range(1, amt + 1):
        dp[0][a] = MAX
    # 状态转移:其余行和列
    for i in range(1, n + 1):
        for a in range(1, amt + 1):
            if coins[i - 1] > a:
                # 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a]
            else:
                # 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)
    return dp[n][amt] if dp[n][amt] != MAX else -1
coin_change.cpp
/* 零钱兑换:动态规划 */
int coinChangeDP(vector<int> &coins, int amt) {
    int n = coins.size();
    int MAX = amt + 1;
    // 初始化 dp 表
    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));
    // 状态转移:首行首列
    for (int a = 1; a <= amt; a++) {
        dp[0][a] = MAX;
    }
    // 状态转移:其余行和列
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[n][amt] != MAX ? dp[n][amt] : -1;
}
coin_change.java
/* 零钱兑换:动态规划 */
int coinChangeDP(int[] coins, int amt) {
    int n = coins.length;
    int MAX = amt + 1;
    // 初始化 dp 表
    int[][] dp = new int[n + 1][amt + 1];
    // 状态转移:首行首列
    for (int a = 1; a <= amt; a++) {
        dp[0][a] = MAX;
    }
    // 状态转移:其余行和列
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[n][amt] != MAX ? dp[n][amt] : -1;
}
coin_change.cs
/* 零钱兑换:动态规划 */
int CoinChangeDP(int[] coins, int amt) {
    int n = coins.Length;
    int MAX = amt + 1;
    // 初始化 dp 表
    int[,] dp = new int[n + 1, amt + 1];
    // 状态转移:首行首列
    for (int a = 1; a <= amt; a++) {
        dp[0, a] = MAX;
    }
    // 状态转移:其余行和列
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i, a] = dp[i - 1, a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[n, amt] != MAX ? dp[n, amt] : -1;
}
coin_change.go
/* 零钱兑换:动态规划 */
func coinChangeDP(coins []int, amt int) int {
    n := len(coins)
    max := amt + 1
    // 初始化 dp 表
    dp := make([][]int, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = make([]int, amt+1)
    }
    // 状态转移:首行首列
    for a := 1; a <= amt; a++ {
        dp[0][a] = max
    }
    // 状态转移:其余行和列
    for i := 1; i <= n; i++ {
        for a := 1; a <= amt; a++ {
            if coins[i-1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i-1][a]
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))
            }
        }
    }
    if dp[n][amt] != max {
        return dp[n][amt]
    }
    return -1
}
coin_change.swift
/* 零钱兑换:动态规划 */
func coinChangeDP(coins: [Int], amt: Int) -> Int {
    let n = coins.count
    let MAX = amt + 1
    // 初始化 dp 表
    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)
    // 状态转移:首行首列
    for a in 1 ... amt {
        dp[0][a] = MAX
    }
    // 状态转移:其余行和列
    for i in 1 ... n {
        for a in 1 ... amt {
            if coins[i - 1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a]
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)
            }
        }
    }
    return dp[n][amt] != MAX ? dp[n][amt] : -1
}
coin_change.js
/* 零钱兑换:动态规划 */
function coinChangeDP(coins, amt) {
    const n = coins.length;
    const MAX = amt + 1;
    // 初始化 dp 表
    const dp = Array.from({ length: n + 1 }, () =>
        Array.from({ length: amt + 1 }, () => 0)
    );
    // 状态转移:首行首列
    for (let a = 1; a <= amt; a++) {
        dp[0][a] = MAX;
    }
    // 状态转移:其余行和列
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[n][amt] !== MAX ? dp[n][amt] : -1;
}
coin_change.ts
/* 零钱兑换:动态规划 */
function coinChangeDP(coins: Array<number>, amt: number): number {
    const n = coins.length;
    const MAX = amt + 1;
    // 初始化 dp 表
    const dp = Array.from({ length: n + 1 }, () =>
        Array.from({ length: amt + 1 }, () => 0)
    );
    // 状态转移:首行首列
    for (let a = 1; a <= amt; a++) {
        dp[0][a] = MAX;
    }
    // 状态转移:其余行和列
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[n][amt] !== MAX ? dp[n][amt] : -1;
}
coin_change.dart
/* 零钱兑换:动态规划 */
int coinChangeDP(List<int> coins, int amt) {
  int n = coins.length;
  int MAX = amt + 1;
  // 初始化 dp 表
  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));
  // 状态转移:首行首列
  for (int a = 1; a <= amt; a++) {
    dp[0][a] = MAX;
  }
  // 状态转移:其余行和列
  for (int i = 1; i <= n; i++) {
    for (int a = 1; a <= amt; a++) {
      if (coins[i - 1] > a) {
        // 若超过目标金额,则不选硬币 i
        dp[i][a] = dp[i - 1][a];
      } else {
        // 不选和选硬币 i 这两种方案的较小值
        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);
      }
    }
  }
  return dp[n][amt] != MAX ? dp[n][amt] : -1;
}
coin_change.rs
/* 零钱兑换:动态规划 */
fn coin_change_dp(coins: &[i32], amt: usize) -> i32 {
    let n = coins.len();
    let max = amt + 1;
    // 初始化 dp 表
    let mut dp = vec![vec![0; amt + 1]; n + 1];
    // 状态转移:首行首列
    for a in 1..=amt {
        dp[0][a] = max;
    }
    // 状态转移:其余行和列
    for i in 1..=n {
        for a in 1..=amt {
            if coins[i - 1] > a as i32 {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);
            }
        }
    }
    if dp[n][amt] != max {
        return dp[n][amt] as i32;
    } else {
        -1
    }
}
coin_change.c
/* 零钱兑换:动态规划 */
int coinChangeDP(int coins[], int amt, int coinsSize) {
    int n = coinsSize;
    int MAX = amt + 1;
    // 初始化 dp 表
    int **dp = malloc((n + 1) * sizeof(int *));
    for (int i = 0; i <= n; i++) {
        dp[i] = calloc(amt + 1, sizeof(int));
    }
    // 状态转移:首行首列
    for (int a = 1; a <= amt; a++) {
        dp[0][a] = MAX;
    }
    // 状态转移:其余行和列
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);
            }
        }
    }
    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;
    // 释放内存
    for (int i = 0; i <= n; i++) {
        free(dp[i]);
    }
    free(dp);
    return res;
}
coin_change.kt
/* 零钱兑换:动态规划 */
fun coinChangeDP(coins: IntArray, amt: Int): Int {
    val n = coins.size
    val MAX = amt + 1
    // 初始化 dp 表
    val dp = Array(n + 1) { IntArray(amt + 1) }
    // 状态转移:首行首列
    for (a in 1..amt) {
        dp[0][a] = MAX
    }
    // 状态转移:其余行和列
    for (i in 1..n) {
        for (a in 1..amt) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a]
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)
            }
        }
    }
    return if (dp[n][amt] != MAX) dp[n][amt] else -1
}
coin_change.rb
[class]{}-[func]{coin_change_dp}
coin_change.zig
// 零钱兑换:动态规划
fn coinChangeDP(comptime coins: []i32, comptime amt: usize) i32 {
    comptime var n = coins.len;
    comptime var max = amt + 1;
    // 初始化 dp 表
    var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1);
    // 状态转移:首行首列
    for (1..amt + 1) |a| {
        dp[0][a] = max;
    }
    // 状态转移:其余行和列
    for (1..n + 1) |i| {
        for (1..amt + 1) |a| {
            if (coins[i - 1] > @as(i32, @intCast(a))) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = @min(dp[i - 1][a], dp[i][a - @as(usize, @intCast(coins[i - 1]))] + 1);
            }
        }
    }
    if (dp[n][amt] != max) {
        return @intCast(dp[n][amt]);
    } else {
        return -1;
    }
}
Code Visualization

Figure 14-25 show the dynamic programming process for the coin change problem, which is very similar to the unbounded knapsack problem.

Dynamic programming process for the coin change problem

coin_change_dp_step2

coin_change_dp_step3

coin_change_dp_step4

coin_change_dp_step5

coin_change_dp_step6

coin_change_dp_step7

coin_change_dp_step8

coin_change_dp_step9

coin_change_dp_step10

coin_change_dp_step11

coin_change_dp_step12

coin_change_dp_step13

coin_change_dp_step14

coin_change_dp_step15

Figure 14-25   Dynamic programming process for the coin change problem

3.   Space optimization

The space optimization for the coin change problem is handled in the same way as for the unbounded knapsack problem:

coin_change.py
def coin_change_dp_comp(coins: list[int], amt: int) -> int:
    """零钱兑换:空间优化后的动态规划"""
    n = len(coins)
    MAX = amt + 1
    # 初始化 dp 表
    dp = [MAX] * (amt + 1)
    dp[0] = 0
    # 状态转移
    for i in range(1, n + 1):
        # 正序遍历
        for a in range(1, amt + 1):
            if coins[i - 1] > a:
                # 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            else:
                # 不选和选硬币 i 这两种方案的较小值
                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)
    return dp[amt] if dp[amt] != MAX else -1
coin_change.cpp
/* 零钱兑换:空间优化后的动态规划 */
int coinChangeDPComp(vector<int> &coins, int amt) {
    int n = coins.size();
    int MAX = amt + 1;
    // 初始化 dp 表
    vector<int> dp(amt + 1, MAX);
    dp[0] = 0;
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[amt] != MAX ? dp[amt] : -1;
}
coin_change.java
/* 零钱兑换:空间优化后的动态规划 */
int coinChangeDPComp(int[] coins, int amt) {
    int n = coins.length;
    int MAX = amt + 1;
    // 初始化 dp 表
    int[] dp = new int[amt + 1];
    Arrays.fill(dp, MAX);
    dp[0] = 0;
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[amt] != MAX ? dp[amt] : -1;
}
coin_change.cs
/* 零钱兑换:空间优化后的动态规划 */
int CoinChangeDPComp(int[] coins, int amt) {
    int n = coins.Length;
    int MAX = amt + 1;
    // 初始化 dp 表
    int[] dp = new int[amt + 1];
    Array.Fill(dp, MAX);
    dp[0] = 0;
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[amt] != MAX ? dp[amt] : -1;
}
coin_change.go
/* 零钱兑换:动态规划 */
func coinChangeDPComp(coins []int, amt int) int {
    n := len(coins)
    max := amt + 1
    // 初始化 dp 表
    dp := make([]int, amt+1)
    for i := 1; i <= amt; i++ {
        dp[i] = max
    }
    // 状态转移
    for i := 1; i <= n; i++ {
        // 正序遍历
        for a := 1; a <= amt; a++ {
            if coins[i-1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))
            }
        }
    }
    if dp[amt] != max {
        return dp[amt]
    }
    return -1
}
coin_change.swift
/* 零钱兑换:空间优化后的动态规划 */
func coinChangeDPComp(coins: [Int], amt: Int) -> Int {
    let n = coins.count
    let MAX = amt + 1
    // 初始化 dp 表
    var dp = Array(repeating: MAX, count: amt + 1)
    dp[0] = 0
    // 状态转移
    for i in 1 ... n {
        for a in 1 ... amt {
            if coins[i - 1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)
            }
        }
    }
    return dp[amt] != MAX ? dp[amt] : -1
}
coin_change.js
/* 零钱兑换:状态压缩后的动态规划 */
function coinChangeDPComp(coins, amt) {
    const n = coins.length;
    const MAX = amt + 1;
    // 初始化 dp 表
    const dp = Array.from({ length: amt + 1 }, () => MAX);
    dp[0] = 0;
    // 状态转移
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[amt] !== MAX ? dp[amt] : -1;
}
coin_change.ts
/* 零钱兑换:状态压缩后的动态规划 */
function coinChangeDPComp(coins: Array<number>, amt: number): number {
    const n = coins.length;
    const MAX = amt + 1;
    // 初始化 dp 表
    const dp = Array.from({ length: amt + 1 }, () => MAX);
    dp[0] = 0;
    // 状态转移
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);
            }
        }
    }
    return dp[amt] !== MAX ? dp[amt] : -1;
}
coin_change.dart
/* 零钱兑换:空间优化后的动态规划 */
int coinChangeDPComp(List<int> coins, int amt) {
  int n = coins.length;
  int MAX = amt + 1;
  // 初始化 dp 表
  List<int> dp = List.filled(amt + 1, MAX);
  dp[0] = 0;
  // 状态转移
  for (int i = 1; i <= n; i++) {
    for (int a = 1; a <= amt; a++) {
      if (coins[i - 1] > a) {
        // 若超过目标金额,则不选硬币 i
        dp[a] = dp[a];
      } else {
        // 不选和选硬币 i 这两种方案的较小值
        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);
      }
    }
  }
  return dp[amt] != MAX ? dp[amt] : -1;
}
coin_change.rs
/* 零钱兑换:空间优化后的动态规划 */
fn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {
    let n = coins.len();
    let max = amt + 1;
    // 初始化 dp 表
    let mut dp = vec![0; amt + 1];
    dp.fill(max);
    dp[0] = 0;
    // 状态转移
    for i in 1..=n {
        for a in 1..=amt {
            if coins[i - 1] > a as i32 {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);
            }
        }
    }
    if dp[amt] != max {
        return dp[amt] as i32;
    } else {
        -1
    }
}
coin_change.c
/* 零钱兑换:空间优化后的动态规划 */
int coinChangeDPComp(int coins[], int amt, int coinsSize) {
    int n = coinsSize;
    int MAX = amt + 1;
    // 初始化 dp 表
    int *dp = malloc((amt + 1) * sizeof(int));
    for (int j = 1; j <= amt; j++) {
        dp[j] = MAX;
    } 
    dp[0] = 0;

    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);
            }
        }
    }
    int res = dp[amt] != MAX ? dp[amt] : -1;
    // 释放内存
    free(dp);
    return res;
}
coin_change.kt
/* 零钱兑换:空间优化后的动态规划 */
fun coinChangeDPComp(coins: IntArray, amt: Int): Int {
    val n = coins.size
    val MAX = amt + 1
    // 初始化 dp 表
    val dp = IntArray(amt + 1)
    dp.fill(MAX)
    dp[0] = 0
    // 状态转移
    for (i in 1..n) {
        for (a in 1..amt) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)
            }
        }
    }
    return if (dp[amt] != MAX) dp[amt] else -1
}
coin_change.rb
[class]{}-[func]{coin_change_dp_comp}
coin_change.zig
// 零钱兑换:空间优化后的动态规划
fn coinChangeDPComp(comptime coins: []i32, comptime amt: usize) i32 {
    comptime var n = coins.len;
    comptime var max = amt + 1;
    // 初始化 dp 表
    var dp = [_]i32{0} ** (amt + 1);
    @memset(&dp, max);
    dp[0] = 0;
    // 状态转移
    for (1..n + 1) |i| {
        for (1..amt + 1) |a| {
            if (coins[i - 1] > @as(i32, @intCast(a))) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = @min(dp[a], dp[a - @as(usize, @intCast(coins[i - 1]))] + 1);
            }
        }
    }
    if (dp[amt] != max) {
        return @intCast(dp[amt]);
    } else {
        return -1;
    }
}
Code Visualization

14.5.3   Coin change problem II

Question

Given \(n\) types of coins, where the denomination of the \(i^{th}\) type of coin is \(coins[i - 1]\), and the target amount is \(amt\). Each type of coin can be selected multiple times, ask how many combinations of coins can make up the target amount. See the example below.

Example data for Coin Change Problem II

Figure 14-26   Example data for Coin Change Problem II

1.   Dynamic programming approach

Compared to the previous problem, the goal of this problem is to determine the number of combinations, so the sub-problem becomes: the number of combinations that can make up amount \(a\) using the first \(i\) types of coins. The \(dp\) table remains a two-dimensional matrix of size \((n+1) \times (amt + 1)\).

The number of combinations for the current state is the sum of the combinations from not selecting the current coin and selecting the current coin. The state transition equation is:

\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \]

When the target amount is \(0\), no coins are needed to make up the target amount, so all \(dp[i, 0]\) in the first column should be initialized to \(1\). When there are no coins, it is impossible to make up any amount >0, so all \(dp[0, a]\) in the first row should be set to \(0\).

2.   Code implementation

coin_change_ii.py
def coin_change_ii_dp(coins: list[int], amt: int) -> int:
    """零钱兑换 II:动态规划"""
    n = len(coins)
    # 初始化 dp 表
    dp = [[0] * (amt + 1) for _ in range(n + 1)]
    # 初始化首列
    for i in range(n + 1):
        dp[i][0] = 1
    # 状态转移
    for i in range(1, n + 1):
        for a in range(1, amt + 1):
            if coins[i - 1] > a:
                # 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a]
            else:
                # 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]
    return dp[n][amt]
coin_change_ii.cpp
/* 零钱兑换 II:动态规划 */
int coinChangeIIDP(vector<int> &coins, int amt) {
    int n = coins.size();
    // 初始化 dp 表
    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));
    // 初始化首列
    for (int i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];
            }
        }
    }
    return dp[n][amt];
}
coin_change_ii.java
/* 零钱兑换 II:动态规划 */
int coinChangeIIDP(int[] coins, int amt) {
    int n = coins.length;
    // 初始化 dp 表
    int[][] dp = new int[n + 1][amt + 1];
    // 初始化首列
    for (int i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];
            }
        }
    }
    return dp[n][amt];
}
coin_change_ii.cs
/* 零钱兑换 II:动态规划 */
int CoinChangeIIDP(int[] coins, int amt) {
    int n = coins.Length;
    // 初始化 dp 表
    int[,] dp = new int[n + 1, amt + 1];
    // 初始化首列
    for (int i = 0; i <= n; i++) {
        dp[i, 0] = 1;
    }
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i, a] = dp[i - 1, a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];
            }
        }
    }
    return dp[n, amt];
}
coin_change_ii.go
/* 零钱兑换 II:动态规划 */
func coinChangeIIDP(coins []int, amt int) int {
    n := len(coins)
    // 初始化 dp 表
    dp := make([][]int, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = make([]int, amt+1)
    }
    // 初始化首列
    for i := 0; i <= n; i++ {
        dp[i][0] = 1
    }
    // 状态转移:其余行和列
    for i := 1; i <= n; i++ {
        for a := 1; a <= amt; a++ {
            if coins[i-1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i-1][a]
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]
            }
        }
    }
    return dp[n][amt]
}
coin_change_ii.swift
/* 零钱兑换 II:动态规划 */
func coinChangeIIDP(coins: [Int], amt: Int) -> Int {
    let n = coins.count
    // 初始化 dp 表
    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)
    // 初始化首列
    for i in 0 ... n {
        dp[i][0] = 1
    }
    // 状态转移
    for i in 1 ... n {
        for a in 1 ... amt {
            if coins[i - 1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a]
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]
            }
        }
    }
    return dp[n][amt]
}
coin_change_ii.js
/* 零钱兑换 II:动态规划 */
function coinChangeIIDP(coins, amt) {
    const n = coins.length;
    // 初始化 dp 表
    const dp = Array.from({ length: n + 1 }, () =>
        Array.from({ length: amt + 1 }, () => 0)
    );
    // 初始化首列
    for (let i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    // 状态转移
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];
            }
        }
    }
    return dp[n][amt];
}
coin_change_ii.ts
/* 零钱兑换 II:动态规划 */
function coinChangeIIDP(coins: Array<number>, amt: number): number {
    const n = coins.length;
    // 初始化 dp 表
    const dp = Array.from({ length: n + 1 }, () =>
        Array.from({ length: amt + 1 }, () => 0)
    );
    // 初始化首列
    for (let i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    // 状态转移
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];
            }
        }
    }
    return dp[n][amt];
}
coin_change_ii.dart
/* 零钱兑换 II:动态规划 */
int coinChangeIIDP(List<int> coins, int amt) {
  int n = coins.length;
  // 初始化 dp 表
  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));
  // 初始化首列
  for (int i = 0; i <= n; i++) {
    dp[i][0] = 1;
  }
  // 状态转移
  for (int i = 1; i <= n; i++) {
    for (int a = 1; a <= amt; a++) {
      if (coins[i - 1] > a) {
        // 若超过目标金额,则不选硬币 i
        dp[i][a] = dp[i - 1][a];
      } else {
        // 不选和选硬币 i 这两种方案之和
        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];
      }
    }
  }
  return dp[n][amt];
}
coin_change_ii.rs
/* 零钱兑换 II:动态规划 */
fn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {
    let n = coins.len();
    // 初始化 dp 表
    let mut dp = vec![vec![0; amt + 1]; n + 1];
    // 初始化首列
    for i in 0..=n {
        dp[i][0] = 1;
    }
    // 状态转移
    for i in 1..=n {
        for a in 1..=amt {
            if coins[i - 1] > a as i32 {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];
            }
        }
    }
    dp[n][amt]
}
coin_change_ii.c
/* 零钱兑换 II:动态规划 */
int coinChangeIIDP(int coins[], int amt, int coinsSize) {
    int n = coinsSize;
    // 初始化 dp 表
    int **dp = malloc((n + 1) * sizeof(int *));
    for (int i = 0; i <= n; i++) {
        dp[i] = calloc(amt + 1, sizeof(int));
    }
    // 初始化首列
    for (int i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];
            }
        }
    }
    int res = dp[n][amt];
    // 释放内存
    for (int i = 0; i <= n; i++) {
        free(dp[i]);
    }
    free(dp);
    return res;
}
coin_change_ii.kt
/* 零钱兑换 II:动态规划 */
fun coinChangeIIDP(coins: IntArray, amt: Int): Int {
    val n = coins.size
    // 初始化 dp 表
    val dp = Array(n + 1) { IntArray(amt + 1) }
    // 初始化首列
    for (i in 0..n) {
        dp[i][0] = 1
    }
    // 状态转移
    for (i in 1..n) {
        for (a in 1..amt) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a]
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]
            }
        }
    }
    return dp[n][amt]
}
coin_change_ii.rb
[class]{}-[func]{coin_change_ii_dp}
coin_change_ii.zig
// 零钱兑换 II:动态规划
fn coinChangeIIDP(comptime coins: []i32, comptime amt: usize) i32 {
    comptime var n = coins.len;
    // 初始化 dp 表
    var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1);
    // 初始化首列
    for (0..n + 1) |i| {
        dp[i][0] = 1;
    }
    // 状态转移
    for (1..n + 1) |i| {
        for (1..amt + 1) |a| {
            if (coins[i - 1] > @as(i32, @intCast(a))) {
                // 若超过目标金额,则不选硬币 i
                dp[i][a] = dp[i - 1][a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[i][a] = dp[i - 1][a] + dp[i][a - @as(usize, @intCast(coins[i - 1]))];
            }
        }
    }
    return dp[n][amt];
}
Code Visualization

3.   Space optimization

The space optimization approach is the same, just remove the coin dimension:

coin_change_ii.py
def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:
    """零钱兑换 II:空间优化后的动态规划"""
    n = len(coins)
    # 初始化 dp 表
    dp = [0] * (amt + 1)
    dp[0] = 1
    # 状态转移
    for i in range(1, n + 1):
        # 正序遍历
        for a in range(1, amt + 1):
            if coins[i - 1] > a:
                # 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            else:
                # 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]]
    return dp[amt]
coin_change_ii.cpp
/* 零钱兑换 II:空间优化后的动态规划 */
int coinChangeIIDPComp(vector<int> &coins, int amt) {
    int n = coins.size();
    // 初始化 dp 表
    vector<int> dp(amt + 1, 0);
    dp[0] = 1;
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]];
            }
        }
    }
    return dp[amt];
}
coin_change_ii.java
/* 零钱兑换 II:空间优化后的动态规划 */
int coinChangeIIDPComp(int[] coins, int amt) {
    int n = coins.length;
    // 初始化 dp 表
    int[] dp = new int[amt + 1];
    dp[0] = 1;
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]];
            }
        }
    }
    return dp[amt];
}
coin_change_ii.cs
/* 零钱兑换 II:空间优化后的动态规划 */
int CoinChangeIIDPComp(int[] coins, int amt) {
    int n = coins.Length;
    // 初始化 dp 表
    int[] dp = new int[amt + 1];
    dp[0] = 1;
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]];
            }
        }
    }
    return dp[amt];
}
coin_change_ii.go
/* 零钱兑换 II:空间优化后的动态规划 */
func coinChangeIIDPComp(coins []int, amt int) int {
    n := len(coins)
    // 初始化 dp 表
    dp := make([]int, amt+1)
    dp[0] = 1
    // 状态转移
    for i := 1; i <= n; i++ {
        // 正序遍历
        for a := 1; a <= amt; a++ {
            if coins[i-1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a-coins[i-1]]
            }
        }
    }
    return dp[amt]
}
coin_change_ii.swift
/* 零钱兑换 II:空间优化后的动态规划 */
func coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {
    let n = coins.count
    // 初始化 dp 表
    var dp = Array(repeating: 0, count: amt + 1)
    dp[0] = 1
    // 状态转移
    for i in 1 ... n {
        for a in 1 ... amt {
            if coins[i - 1] > a {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]]
            }
        }
    }
    return dp[amt]
}
coin_change_ii.js
/* 零钱兑换 II:状态压缩后的动态规划 */
function coinChangeIIDPComp(coins, amt) {
    const n = coins.length;
    // 初始化 dp 表
    const dp = Array.from({ length: amt + 1 }, () => 0);
    dp[0] = 1;
    // 状态转移
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]];
            }
        }
    }
    return dp[amt];
}
coin_change_ii.ts
/* 零钱兑换 II:状态压缩后的动态规划 */
function coinChangeIIDPComp(coins: Array<number>, amt: number): number {
    const n = coins.length;
    // 初始化 dp 表
    const dp = Array.from({ length: amt + 1 }, () => 0);
    dp[0] = 1;
    // 状态转移
    for (let i = 1; i <= n; i++) {
        for (let a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]];
            }
        }
    }
    return dp[amt];
}
coin_change_ii.dart
/* 零钱兑换 II:空间优化后的动态规划 */
int coinChangeIIDPComp(List<int> coins, int amt) {
  int n = coins.length;
  // 初始化 dp 表
  List<int> dp = List.filled(amt + 1, 0);
  dp[0] = 1;
  // 状态转移
  for (int i = 1; i <= n; i++) {
    for (int a = 1; a <= amt; a++) {
      if (coins[i - 1] > a) {
        // 若超过目标金额,则不选硬币 i
        dp[a] = dp[a];
      } else {
        // 不选和选硬币 i 这两种方案之和
        dp[a] = dp[a] + dp[a - coins[i - 1]];
      }
    }
  }
  return dp[amt];
}
coin_change_ii.rs
/* 零钱兑换 II:空间优化后的动态规划 */
fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {
    let n = coins.len();
    // 初始化 dp 表
    let mut dp = vec![0; amt + 1];
    dp[0] = 1;
    // 状态转移
    for i in 1..=n {
        for a in 1..=amt {
            if coins[i - 1] > a as i32 {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];
            }
        }
    }
    dp[amt]
}
coin_change_ii.c
/* 零钱兑换 II:空间优化后的动态规划 */
int coinChangeIIDPComp(int coins[], int amt, int coinsSize) {
    int n = coinsSize;
    // 初始化 dp 表
    int *dp = calloc(amt + 1, sizeof(int));
    dp[0] = 1;
    // 状态转移
    for (int i = 1; i <= n; i++) {
        for (int a = 1; a <= amt; a++) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]];
            }
        }
    }
    int res = dp[amt];
    // 释放内存
    free(dp);
    return res;
}
coin_change_ii.kt
/* 零钱兑换 II:空间优化后的动态规划 */
fun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {
    val n = coins.size
    // 初始化 dp 表
    val dp = IntArray(amt + 1)
    dp[0] = 1
    // 状态转移
    for (i in 1..n) {
        for (a in 1..amt) {
            if (coins[i - 1] > a) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a]
            } else {
                // 不选和选硬币 i 这两种方案之和
                dp[a] = dp[a] + dp[a - coins[i - 1]]
            }
        }
    }
    return dp[amt]
}
coin_change_ii.rb
[class]{}-[func]{coin_change_ii_dp_comp}
coin_change_ii.zig
// 零钱兑换 II:空间优化后的动态规划
fn coinChangeIIDPComp(comptime coins: []i32, comptime amt: usize) i32 {
    comptime var n = coins.len;
    // 初始化 dp 表
    var dp = [_]i32{0} ** (amt + 1);
    dp[0] = 1;
    // 状态转移
    for (1..n + 1) |i| {
        for (1..amt + 1) |a| {
            if (coins[i - 1] > @as(i32, @intCast(a))) {
                // 若超过目标金额,则不选硬币 i
                dp[a] = dp[a];
            } else {
                // 不选和选硬币 i 这两种方案的较小值
                dp[a] = dp[a] + dp[a - @as(usize, @intCast(coins[i - 1]))];
            }
        }
    }
    return dp[amt];
}
Code Visualization

Feel free to drop your insights, questions or suggestions