This commit is contained in:
krahets
2023-07-26 03:16:04 +08:00
parent a71f51e5b8
commit fd34c845bc
15 changed files with 208 additions and 187 deletions

View File

@@ -2861,10 +2861,10 @@
<li class="md-nav__item">
<a href="#1451" class="md-nav__link">
14.5.1. &nbsp; 完全背包问题
14.5.1. &nbsp; 完全背包
</a>
<nav class="md-nav" aria-label="14.5.1.   完全背包问题">
<nav class="md-nav" aria-label="14.5.1.   完全背包">
<ul class="md-nav__list">
<li class="md-nav__item">
@@ -3413,10 +3413,10 @@
<li class="md-nav__item">
<a href="#1451" class="md-nav__link">
14.5.1. &nbsp; 完全背包问题
14.5.1. &nbsp; 完全背包
</a>
<nav class="md-nav" aria-label="14.5.1.   完全背包问题">
<nav class="md-nav" aria-label="14.5.1.   完全背包">
<ul class="md-nav__list">
<li class="md-nav__item">
@@ -3516,11 +3516,11 @@
<h1 id="145">14.5. &nbsp; 完全背包问题<a class="headerlink" href="#145" title="Permanent link">&para;</a></h1>
<p>在本节,我们先求解 0-1 背包的一个变种问题:完全背包问题;再了解完全背包的一种特例问题:零钱兑换。</p>
<h2 id="1451">14.5.1. &nbsp; 完全背包问题<a class="headerlink" href="#1451" title="Permanent link">&para;</a></h2>
<p>在本节,我们先求解另一个常见的背包问题:完全背包,再了解它的一种特例:零钱兑换。</p>
<h2 id="1451">14.5.1. &nbsp; 完全背包<a class="headerlink" href="#1451" title="Permanent link">&para;</a></h2>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>给定 <span class="arithmatex">\(n\)</span> 物品,第 <span class="arithmatex">\(i\)</span> 个物品的重量为 <span class="arithmatex">\(wgt[i-1]\)</span> 、价值为 <span class="arithmatex">\(val[i-1]\)</span> 现在有个容量为 <span class="arithmatex">\(cap\)</span> 的背包<strong>物品可以重复选取</strong>,问在不超过背包容量下背包中物品的最大价值。</p>
<p>给定 <span class="arithmatex">\(n\)</span> 物品,第 <span class="arithmatex">\(i\)</span> 个物品的重量为 <span class="arithmatex">\(wgt[i-1]\)</span> 、价值为 <span class="arithmatex">\(val[i-1]\)</span> 和一个容量为 <span class="arithmatex">\(cap\)</span> 的背包<strong>物品可以重复选取</strong>,问在不超过背包容量下能放入物品的最大价值。</p>
</div>
<p><img alt="完全背包问题的示例数据" src="../unbounded_knapsack_problem.assets/unbounded_knapsack_example.png" /></p>
<p align="center"> Fig. 完全背包问题的示例数据 </p>
@@ -3533,14 +3533,14 @@
<p>这就导致了状态转移的变化,对于状态 <span class="arithmatex">\([i, c]\)</span> 有:</p>
<ul>
<li><strong>不放入物品 <span class="arithmatex">\(i\)</span></strong> :与 0-1 背包相同,转移至 <span class="arithmatex">\([i-1, c]\)</span> </li>
<li><strong>放入物品 <span class="arithmatex">\(i\)</span></strong> 状态转移至 <span class="arithmatex">\([i, c-wgt[i-1]]\)</span> 而非 0-1 背包 <span class="arithmatex">\([i-1, c-wgt[i-1]]\)</span> </li>
<li><strong>放入物品 <span class="arithmatex">\(i\)</span></strong> 0-1 背包不同,转移至 <span class="arithmatex">\([i, c-wgt[i-1]]\)</span> </li>
</ul>
<p>因此状态转移方程变为:</p>
<p>从而状态转移方程变为:</p>
<div class="arithmatex">\[
dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
\]</div>
<h3 id="_1">代码实现<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<p>对比两道题目的动态规划代码,状态转移中有一处从 <span class="arithmatex">\(i-1\)</span> 变为 <span class="arithmatex">\(i\)</span> ,其余完全一致。</p>
<p>对比两道题目的代码,状态转移中有一处从 <span class="arithmatex">\(i-1\)</span> 变为 <span class="arithmatex">\(i\)</span> ,其余完全一致。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:11"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JavaScript</label><label for="__tabbed_1_6">TypeScript</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -3715,7 +3715,8 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
</div>
</div>
<h3 id="_2">状态压缩<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<p>由于当前状态是从左边和上边的状态转移而来,<strong>因此状态压缩后应该对 <span class="arithmatex">\(dp\)</span> 表中的每一行采取正序遍历</strong>,这个遍历顺序与 0-1 背包正好相反。请通过以下动画来理解为什么要改为正序遍历</p>
<p>由于当前状态是从左边和上边的状态转移而来,<strong>因此状态压缩后应该对 <span class="arithmatex">\(dp\)</span> 表中的每一行采取正序遍历</strong></p>
<p>这个遍历顺序与 0-1 背包正好相反。请通过以下动画来理解两者的区别。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:6"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">&lt;1&gt;</label><label for="__tabbed_2_2">&lt;2&gt;</label><label for="__tabbed_2_3">&lt;3&gt;</label><label for="__tabbed_2_4">&lt;4&gt;</label><label for="__tabbed_2_5">&lt;5&gt;</label><label for="__tabbed_2_6">&lt;6&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -3916,14 +3917,13 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
<p class="admonition-title">Question</p>
<p>给定 <span class="arithmatex">\(n\)</span> 种硬币,第 <span class="arithmatex">\(i\)</span> 个硬币的面值为 <span class="arithmatex">\(coins[i - 1]\)</span> ,目标金额为 <span class="arithmatex">\(amt\)</span> <strong>每种硬币可以重复选取</strong>,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 <span class="arithmatex">\(-1\)</span></p>
</div>
<p>如下图所示,凑出 <span class="arithmatex">\(11\)</span> 元最少需要 <span class="arithmatex">\(3\)</span> 枚硬币,方案为 <span class="arithmatex">\(1 + 2 + 5 = 11\)</span></p>
<p><img alt="零钱兑换问题的示例数据" src="../unbounded_knapsack_problem.assets/coin_change_example.png" /></p>
<p align="center"> Fig. 零钱兑换问题的示例数据 </p>
<p><strong>零钱兑换问题可以看作是完全背包问题的一种特殊情况</strong>,两者具有以下联系与不同点:</p>
<p><strong>零钱兑换可以看作是完全背包的一种特殊情况</strong>,两者具有以下联系与不同点:</p>
<ul>
<li>两道题可以相互转换,“物品”对应于“硬币”、“物品重量”对应于“硬币面值”、“背包容量”对应于“目标金额”;</li>
<li>目标不同,背包问题是要最大化物品价值,零钱兑换问题是要最小化硬币数量;</li>
<li>优化目标相反,背包问题是要最大化物品价值,零钱兑换问题是要最小化硬币数量;</li>
<li>背包问题是求“不超过”背包容量下的解,零钱兑换是求“恰好”凑到目标金额的解;</li>
</ul>
<p><strong>第一步:思考每轮的决策,定义状态,从而得到 <span class="arithmatex">\(dp\)</span></strong></p>
@@ -3933,17 +3933,18 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
<p>与完全背包的状态转移方程基本相同,不同点在于:</p>
<ul>
<li>本题要求最小值,因此需将运算符 <span class="arithmatex">\(\max()\)</span> 更改为 <span class="arithmatex">\(\min()\)</span> </li>
<li>优化主体是硬币数量而非商品价值,因此在选中硬币时执行 <span class="arithmatex">\(+1\)</span> 即可;</li>
<li>优化主体是硬币数量而非商品价值,因此在选中硬币时执行 <span class="arithmatex">\(+1\)</span> 即可;</li>
</ul>
<div class="arithmatex">\[
dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
\]</div>
<p><strong>第三步:确定边界条件和状态转移顺序</strong></p>
<p>当目标金额为 <span class="arithmatex">\(0\)</span> 时,凑出它的最少硬币个数为 <span class="arithmatex">\(0\)</span> ,即所有 <span class="arithmatex">\(dp[i, 0]\)</span> 都等于 <span class="arithmatex">\(0\)</span> 。当无硬币时,<strong>无法凑出任意 <span class="arithmatex">\(&gt; 0\)</span> 的目标金额</strong>,即是无效解。为使状态转移方程中的 <span class="arithmatex">\(\min()\)</span> 函数能够识别并过滤无效解,我们考虑使用 <span class="arithmatex">\(+ \infty\)</span> 来表示它们,即令所有 <span class="arithmatex">\(dp[0, a]\)</span> 都等于 <span class="arithmatex">\(+ \infty\)</span></p>
<p>当目标金额为 <span class="arithmatex">\(0\)</span> 时,凑出它的最少硬币个数为 <span class="arithmatex">\(0\)</span> ,即首列所有 <span class="arithmatex">\(dp[i, 0]\)</span> 都等于 <span class="arithmatex">\(0\)</span></p>
<p>当无硬币时,<strong>无法凑出任意 <span class="arithmatex">\(&gt; 0\)</span> 的目标金额</strong>,即是无效解。为使状态转移方程中的 <span class="arithmatex">\(\min()\)</span> 函数能够识别并过滤无效解,我们考虑使用 <span class="arithmatex">\(+ \infty\)</span> 来表示它们,即令首行所有 <span class="arithmatex">\(dp[0, a]\)</span> 都等于 <span class="arithmatex">\(+ \infty\)</span></p>
<h3 id="_3">代码实现<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<p>然而,大多数编程语言并未提供 <span class="arithmatex">\(+ \infty\)</span> 变量,因此只能使用整型 <code>int</code> 的最大值来代替而这又会导致大数越界:<strong><span class="arithmatex">\(dp[i, a - coins[i-1]]\)</span> 是无效解时,再执行 <span class="arithmatex">\(+ 1\)</span> 操作发生溢出</strong></p>
<p>解决该问题,我们采用一个不可能达到的大数字 <span class="arithmatex">\(amt + 1\)</span> 来表示无效解,因为凑出 <span class="arithmatex">\(amt\)</span> 的硬币个数最多为 <span class="arithmatex">\(amt\)</span> 个。</p>
<p>最后返回前,判断 <span class="arithmatex">\(dp[n, amt]\)</span> 是否等于 <span class="arithmatex">\(amt + 1\)</span> ,若是则返回 <span class="arithmatex">\(-1\)</span> ,代表无法凑出目标金额。</p>
<p>大多数编程语言并未提供 <span class="arithmatex">\(+ \infty\)</span> 变量,只能使用整型 <code>int</code> 的最大值来代替而这又会导致大数越界:状态转移方程中的 <span class="arithmatex">\(+ 1\)</span> 操作可能发生溢出。</p>
<p>此,我们采用数字 <span class="arithmatex">\(amt + 1\)</span> 来表示无效解,因为凑出 <span class="arithmatex">\(amt\)</span> 的硬币个数最多为 <span class="arithmatex">\(amt\)</span> 个。</p>
<p>最后返回前,判断 <span class="arithmatex">\(dp[n, amt]\)</span> 是否等于 <span class="arithmatex">\(amt + 1\)</span> ,若是则返回 <span class="arithmatex">\(-1\)</span> ,代表无法凑出目标金额。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:11"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Java</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Python</label><label for="__tabbed_4_4">Go</label><label for="__tabbed_4_5">JavaScript</label><label for="__tabbed_4_6">TypeScript</label><label for="__tabbed_4_7">C</label><label for="__tabbed_4_8">C#</label><label for="__tabbed_4_9">Swift</label><label for="__tabbed_4_10">Zig</label><label for="__tabbed_4_11">Dart</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4158,7 +4159,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
</div>
</div>
</div>
<p>下图展示了零钱兑换的动态规划过程。</p>
<p>下图展示了零钱兑换的动态规划过程,和完全背包非常相似</p>
<div class="tabbed-set tabbed-alternate" data-tabs="5:15"><input checked="checked" id="__tabbed_5_1" name="__tabbed_5" type="radio" /><input id="__tabbed_5_2" name="__tabbed_5" type="radio" /><input id="__tabbed_5_3" name="__tabbed_5" type="radio" /><input id="__tabbed_5_4" name="__tabbed_5" type="radio" /><input id="__tabbed_5_5" name="__tabbed_5" type="radio" /><input id="__tabbed_5_6" name="__tabbed_5" type="radio" /><input id="__tabbed_5_7" name="__tabbed_5" type="radio" /><input id="__tabbed_5_8" name="__tabbed_5" type="radio" /><input id="__tabbed_5_9" name="__tabbed_5" type="radio" /><input id="__tabbed_5_10" name="__tabbed_5" type="radio" /><input id="__tabbed_5_11" name="__tabbed_5" type="radio" /><input id="__tabbed_5_12" name="__tabbed_5" type="radio" /><input id="__tabbed_5_13" name="__tabbed_5" type="radio" /><input id="__tabbed_5_14" name="__tabbed_5" type="radio" /><input id="__tabbed_5_15" name="__tabbed_5" type="radio" /><div class="tabbed-labels"><label for="__tabbed_5_1">&lt;1&gt;</label><label for="__tabbed_5_2">&lt;2&gt;</label><label for="__tabbed_5_3">&lt;3&gt;</label><label for="__tabbed_5_4">&lt;4&gt;</label><label for="__tabbed_5_5">&lt;5&gt;</label><label for="__tabbed_5_6">&lt;6&gt;</label><label for="__tabbed_5_7">&lt;7&gt;</label><label for="__tabbed_5_8">&lt;8&gt;</label><label for="__tabbed_5_9">&lt;9&gt;</label><label for="__tabbed_5_10">&lt;10&gt;</label><label for="__tabbed_5_11">&lt;11&gt;</label><label for="__tabbed_5_12">&lt;12&gt;</label><label for="__tabbed_5_13">&lt;13&gt;</label><label for="__tabbed_5_14">&lt;14&gt;</label><label for="__tabbed_5_15">&lt;15&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4209,7 +4210,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
</div>
</div>
<h3 id="_4">状态压缩<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h3>
<p>由于零钱兑换和完全背包的状态转移方程如出一辙,因此状态压缩方式也相同</p>
<p>零钱兑换的状态压缩的处理方式和完全背包一致</p>
<div class="tabbed-set tabbed-alternate" data-tabs="6:11"><input checked="checked" id="__tabbed_6_1" name="__tabbed_6" type="radio" /><input id="__tabbed_6_2" name="__tabbed_6" type="radio" /><input id="__tabbed_6_3" name="__tabbed_6" type="radio" /><input id="__tabbed_6_4" name="__tabbed_6" type="radio" /><input id="__tabbed_6_5" name="__tabbed_6" type="radio" /><input id="__tabbed_6_6" name="__tabbed_6" type="radio" /><input id="__tabbed_6_7" name="__tabbed_6" type="radio" /><input id="__tabbed_6_8" name="__tabbed_6" type="radio" /><input id="__tabbed_6_9" name="__tabbed_6" type="radio" /><input id="__tabbed_6_10" name="__tabbed_6" type="radio" /><input id="__tabbed_6_11" name="__tabbed_6" type="radio" /><div class="tabbed-labels"><label for="__tabbed_6_1">Java</label><label for="__tabbed_6_2">C++</label><label for="__tabbed_6_3">Python</label><label for="__tabbed_6_4">Go</label><label for="__tabbed_6_5">JavaScript</label><label for="__tabbed_6_6">TypeScript</label><label for="__tabbed_6_7">C</label><label for="__tabbed_6_8">C#</label><label for="__tabbed_6_9">Swift</label><label for="__tabbed_6_10">Zig</label><label for="__tabbed_6_11">Dart</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4421,7 +4422,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
<div class="arithmatex">\[
dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
\]</div>
<p>当目标金额为 <span class="arithmatex">\(0\)</span> 时,无需选择任何硬币即可凑出目标金额,因此应将所有 <span class="arithmatex">\(dp[i, 0]\)</span> 都初始化为 <span class="arithmatex">\(1\)</span> 。当无硬币时,无法凑出任何 <span class="arithmatex">\(&gt;0\)</span> 的目标金额,因此所有 <span class="arithmatex">\(dp[0, a]\)</span> 都等于 <span class="arithmatex">\(0\)</span></p>
<p>当目标金额为 <span class="arithmatex">\(0\)</span> 时,无需选择任何硬币即可凑出目标金额,因此应将首列所有 <span class="arithmatex">\(dp[i, 0]\)</span> 都初始化为 <span class="arithmatex">\(1\)</span> 。当无硬币时,无法凑出任何 <span class="arithmatex">\(&gt;0\)</span> 的目标金额,因此首行所有 <span class="arithmatex">\(dp[0, a]\)</span> 都等于 <span class="arithmatex">\(0\)</span></p>
<h3 id="_5">代码实现<a class="headerlink" href="#_5" title="Permanent link">&para;</a></h3>
<div class="tabbed-set tabbed-alternate" data-tabs="7:11"><input checked="checked" id="__tabbed_7_1" name="__tabbed_7" type="radio" /><input id="__tabbed_7_2" name="__tabbed_7" type="radio" /><input id="__tabbed_7_3" name="__tabbed_7" type="radio" /><input id="__tabbed_7_4" name="__tabbed_7" type="radio" /><input id="__tabbed_7_5" name="__tabbed_7" type="radio" /><input id="__tabbed_7_6" name="__tabbed_7" type="radio" /><input id="__tabbed_7_7" name="__tabbed_7" type="radio" /><input id="__tabbed_7_8" name="__tabbed_7" type="radio" /><input id="__tabbed_7_9" name="__tabbed_7" type="radio" /><input id="__tabbed_7_10" name="__tabbed_7" type="radio" /><input id="__tabbed_7_11" name="__tabbed_7" type="radio" /><div class="tabbed-labels"><label for="__tabbed_7_1">Java</label><label for="__tabbed_7_2">C++</label><label for="__tabbed_7_3">Python</label><label for="__tabbed_7_4">Go</label><label for="__tabbed_7_5">JavaScript</label><label for="__tabbed_7_6">TypeScript</label><label for="__tabbed_7_7">C</label><label for="__tabbed_7_8">C#</label><label for="__tabbed_7_9">Swift</label><label for="__tabbed_7_10">Zig</label><label for="__tabbed_7_11">Dart</label></div>
<div class="tabbed-content">