docs/chapter_dynamic_programming/unbounded_knapsack_problem.md
在本节中,我们先求解另一个常见的背包问题:完全背包,再了解它的一种特例:零钱兑换。
!!! question
给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$、价值为 $val[i-1]$ ,和一个容量为 $cap$ 的背包。**每个物品可以重复选取**,问在限定背包容量下能放入物品的最大价值。示例如下图所示。
完全背包问题和 0-1 背包问题非常相似,区别仅在于不限制物品的选择次数。
在完全背包问题的规定下,状态 $[i, c]$ 的变化分为两种情况。
从而状态转移方程变为:
$$ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) $$
对比两道题目的代码,状态转移中有一处从 $i-1$ 变为 $i$ ,其余完全一致:
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp}
由于当前状态是从左边和上边的状态转移而来的,因此空间优化后应该对 $dp$ 表中的每一行进行正序遍历。
这个遍历顺序与 0-1 背包正好相反。请借助下图来理解两者的区别。
=== "<1>"
=== "<2>"
=== "<3>"
=== "<4>"
=== "<5>"
=== "<6>"
代码实现比较简单,仅需将数组 dp 的第一维删除:
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp_comp}
背包问题是一大类动态规划问题的代表,其拥有很多变种,例如零钱兑换问题。
!!! question
给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,**每种硬币可以重复选取**,问能够凑出目标金额的最少硬币数量。如果无法凑出目标金额,则返回 $-1$ 。示例如下图所示。
零钱兑换可以看作完全背包问题的一种特殊情况,两者具有以下联系与不同点。
第一步:思考每轮的决策,定义状态,从而得到 $dp$ 表
状态 $[i, a]$ 对应的子问题为:前 $i$ 种硬币能够凑出金额 $a$ 的最少硬币数量,记为 $dp[i, a]$ 。
二维 $dp$ 表的尺寸为 $(n+1) \times (amt+1)$ 。
第二步:找出最优子结构,进而推导出状态转移方程
本题与完全背包问题的状态转移方程存在以下两点差异。
$$ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) $$
第三步:确定边界条件和状态转移顺序
当目标金额为 $0$ 时,凑出它的最少硬币数量为 $0$ ,即首列所有 $dp[i, 0]$ 都等于 $0$ 。
当无硬币时,无法凑出任意 $> 0$ 的目标金额,即是无效解。为使状态转移方程中的 $\min()$ 函数能够识别并过滤无效解,我们考虑使用 $+ \infty$ 来表示它们,即令首行所有 $dp[0, a]$ 都等于 $+ \infty$ 。
大多数编程语言并未提供 $+ \infty$ 变量,只能使用整型 int 的最大值来代替。而这又会导致大数越界:状态转移方程中的 $+ 1$ 操作可能发生溢出。
为此,我们采用数字 $amt + 1$ 来表示无效解,因为凑出 $amt$ 的硬币数量最多为 $amt$ 。最后返回前,判断 $dp[n, amt]$ 是否等于 $amt + 1$ ,若是则返回 $-1$ ,代表无法凑出目标金额。代码如下所示:
[file]{coin_change}-[class]{}-[func]{coin_change_dp}
下图展示了零钱兑换的动态规划过程,和完全背包问题非常相似。
=== "<1>"
=== "<2>"
=== "<3>"
=== "<4>"
=== "<5>"
=== "<6>"
=== "<7>"
=== "<8>"
=== "<9>"
=== "<10>"
=== "<11>"
=== "<12>"
=== "<13>"
=== "<14>"
=== "<15>"
零钱兑换的空间优化的处理方式和完全背包问题一致:
[file]{coin_change}-[class]{}-[func]{coin_change_dp_comp}
!!! question
给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,**问凑出目标金额的硬币组合数量**。示例如下图所示。
相比于上一题,本题目标是求组合数量,因此子问题变为:前 $i$ 种硬币能够凑出金额 $a$ 的组合数量。而 $dp$ 表仍然是尺寸为 $(n+1) \times (amt + 1)$ 的二维矩阵。
当前状态的组合数量等于不选当前硬币与选当前硬币这两种决策的组合数量之和。状态转移方程为:
$$ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] $$
当目标金额为 $0$ 时,无须选择任何硬币即可凑出目标金额,因此应将首列所有 $dp[i, 0]$ 都初始化为 $1$ 。当无硬币时,无法凑出任何 $>0$ 的目标金额,因此首行所有 $dp[0, a]$ 都等于 $0$ 。
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp}
空间优化处理方式相同,删除硬币维度即可:
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp_comp}