This commit is contained in:
krahets
2023-08-20 13:37:20 +08:00
parent 88e0b11361
commit 96fded547b
35 changed files with 777 additions and 716 deletions
@@ -2857,29 +2857,29 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
方法一:暴力搜索
<a href="#1" class="md-nav__link">
1. &nbsp; 方法一:暴力搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
方法二:记忆化搜索
<a href="#2" class="md-nav__link">
2. &nbsp; 方法二:记忆化搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
方法三:动态规划
<a href="#3" class="md-nav__link">
3. &nbsp; 方法三:动态规划
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
状态压缩
<a href="#4" class="md-nav__link">
4. &nbsp; 状态压缩
</a>
</li>
@@ -3432,29 +3432,29 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
方法一:暴力搜索
<a href="#1" class="md-nav__link">
1. &nbsp; 方法一:暴力搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
方法二:记忆化搜索
<a href="#2" class="md-nav__link">
2. &nbsp; 方法二:记忆化搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
方法三:动态规划
<a href="#3" class="md-nav__link">
3. &nbsp; 方法三:动态规划
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
状态压缩
<a href="#4" class="md-nav__link">
4. &nbsp; 状态压缩
</a>
</li>
@@ -3557,7 +3557,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
<p>状态转移顺序的核心是要保证在计算当前问题的解时,所有它依赖的更小子问题的解都已经被正确地计算出来。</p>
</div>
<p>根据以上分析,我们已经可以直接写出动态规划代码。然而子问题分解是一种从顶至底的思想,因此按照“暴力搜索 <span class="arithmatex">\(\rightarrow\)</span> 记忆化搜索 <span class="arithmatex">\(\rightarrow\)</span> 动态规划”的顺序实现更加符合思维习惯。</p>
<h3 id="_1">方法一:暴力搜索<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<h3 id="1">1. &nbsp; 方法一:暴力搜索<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>从状态 <span class="arithmatex">\([i, j]\)</span> 开始搜索,不断分解为更小的状态 <span class="arithmatex">\([i-1, j]\)</span><span class="arithmatex">\([i, j-1]\)</span> ,包括以下递归要素:</p>
<ul>
<li><strong>递归参数</strong>:状态 <span class="arithmatex">\([i, j]\)</span></li>
@@ -3756,7 +3756,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
<p align="center"> 图:暴力搜索递归树 </p>
<p>每个状态都有向下和向右两种选择,从左上角走到右下角总共需要 <span class="arithmatex">\(m + n - 2\)</span> 步,所以最差时间复杂度为 <span class="arithmatex">\(O(2^{m + n})\)</span> 。请注意,这种计算方式未考虑临近网格边界的情况,当到达网络边界时只剩下一种选择。因此实际的路径数量会少一些。</p>
<h3 id="_2">方法二:记忆化搜索<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<h3 id="2">2. &nbsp; 方法二:记忆化搜索<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>我们引入一个和网格 <code>grid</code> 相同尺寸的记忆列表 <code>mem</code> ,用于记录各个子问题的解,并将重叠子问题进行剪枝。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:12"><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" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JS</label><label for="__tabbed_2_6">TS</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label><label for="__tabbed_2_11">Dart</label><label for="__tabbed_2_12">Rust</label></div>
<div class="tabbed-content">
@@ -3994,7 +3994,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
<p><img alt="记忆化搜索递归树" src="../dp_solution_pipeline.assets/min_path_sum_dfs_mem.png" /></p>
<p align="center"> 图:记忆化搜索递归树 </p>
<h3 id="_3">方法三:动态规划<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<h3 id="3">3. &nbsp; 方法三:动态规划<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>基于迭代实现动态规划解法。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:12"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JS</label><label for="__tabbed_3_6">TS</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label><label for="__tabbed_3_11">Dart</label><label for="__tabbed_3_12">Rust</label></div>
<div class="tabbed-content">
@@ -4281,7 +4281,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
</div>
<p align="center"> 图:最小路径和的动态规划过程 </p>
<h3 id="_4">状态压缩<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h3>
<h3 id="4">4. &nbsp; 状态压缩<a class="headerlink" href="#4" title="Permanent link">&para;</a></h3>
<p>由于每个格子只与其左边和上边的格子有关,因此我们可以只用一个单行数组来实现 <span class="arithmatex">\(dp\)</span> 表。</p>
<p>请注意,因为数组 <code>dp</code> 只能表示一行的状态,所以我们无法提前初始化首列状态,而是在遍历每行中更新它。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="5:12"><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" /><div class="tabbed-labels"><label for="__tabbed_5_1">Java</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Python</label><label for="__tabbed_5_4">Go</label><label for="__tabbed_5_5">JS</label><label for="__tabbed_5_6">TS</label><label for="__tabbed_5_7">C</label><label for="__tabbed_5_8">C#</label><label for="__tabbed_5_9">Swift</label><label for="__tabbed_5_10">Zig</label><label for="__tabbed_5_11">Dart</label><label for="__tabbed_5_12">Rust</label></div>
@@ -2926,15 +2926,22 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
代码实现
<a href="#1" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
状态压缩
<a href="#2" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -3383,15 +3390,22 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
代码实现
<a href="#1" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
状态压缩
<a href="#2" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -3436,6 +3450,7 @@
<p><img alt="基于决策树模型表示编辑距离问题" src="../edit_distance_problem.assets/edit_distance_decision_tree.png" /></p>
<p align="center"> 图:基于决策树模型表示编辑距离问题 </p>
<h3 id="1">1. &nbsp; 动态规划思路<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p><strong>第一步:思考每轮的决策,定义状态,从而得到 <span class="arithmatex">\(dp\)</span></strong></p>
<p>每一轮的决策是对字符串 <span class="arithmatex">\(s\)</span> 进行一次编辑操作。</p>
<p>我们希望在编辑操作的过程中,问题的规模逐渐缩小,这样才能构建子问题。设字符串 <span class="arithmatex">\(s\)</span><span class="arithmatex">\(t\)</span> 的长度分别为 <span class="arithmatex">\(n\)</span><span class="arithmatex">\(m\)</span> ,我们先考虑两字符串尾部的字符 <span class="arithmatex">\(s[n-1]\)</span><span class="arithmatex">\(t[m-1]\)</span> </p>
@@ -3467,7 +3482,7 @@ dp[i, j] = dp[i-1, j-1]
<p><strong>第三步:确定边界条件和状态转移顺序</strong></p>
<p>当两字符串都为空时,编辑步数为 <span class="arithmatex">\(0\)</span> ,即 <span class="arithmatex">\(dp[0, 0] = 0\)</span> 。当 <span class="arithmatex">\(s\)</span> 为空但 <span class="arithmatex">\(t\)</span> 不为空时,最少编辑步数等于 <span class="arithmatex">\(t\)</span> 的长度,即首行 <span class="arithmatex">\(dp[0, j] = j\)</span> 。当 <span class="arithmatex">\(s\)</span> 不为空但 <span class="arithmatex">\(t\)</span> 为空时,等于 <span class="arithmatex">\(s\)</span> 的长度,即首列 <span class="arithmatex">\(dp[i, 0] = i\)</span></p>
<p>观察状态转移方程,解 <span class="arithmatex">\(dp[i, j]\)</span> 依赖左方、上方、左上方的解,因此通过两层循环正序遍历整个 <span class="arithmatex">\(dp\)</span> 表即可。</p>
<h3 id="_1">代码实现<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<h3 id="2">2. &nbsp; 代码实现<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><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" /><input id="__tabbed_1_12" 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">JS</label><label for="__tabbed_1_6">TS</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><label for="__tabbed_1_12">Rust</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -3788,7 +3803,7 @@ dp[i, j] = dp[i-1, j-1]
</div>
<p align="center"> 图:编辑距离的动态规划过程 </p>
<h3 id="_2">状态压缩<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<h3 id="3">3. &nbsp; 状态压缩<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>由于 <span class="arithmatex">\(dp[i,j]\)</span> 是由上方 <span class="arithmatex">\(dp[i-1, j]\)</span> 、左方 <span class="arithmatex">\(dp[i, j-1]\)</span> 、左上方状态 <span class="arithmatex">\(dp[i-1, j-1]\)</span> 转移而来,而正序遍历会丢失左上方 <span class="arithmatex">\(dp[i-1, j-1]\)</span> ,倒序遍历无法提前构建 <span class="arithmatex">\(dp[i, j-1]\)</span> ,因此两种遍历顺序都不可取。</p>
<p>为此,我们可以使用一个变量 <code>leftup</code> 来暂存左上方的解 <span class="arithmatex">\(dp[i-1, j-1]\)</span> ,从而只需考虑左方和上方的解。此时的情况与完全背包问题相同,可使用正序遍历。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:12"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JS</label><label for="__tabbed_3_6">TS</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label><label for="__tabbed_3_11">Dart</label><label for="__tabbed_3_12">Rust</label></div>
@@ -2870,29 +2870,29 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
方法一:暴力搜索
<a href="#1" class="md-nav__link">
1. &nbsp; 方法一:暴力搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
方法二:记忆化搜索
<a href="#2" class="md-nav__link">
2. &nbsp; 方法二:记忆化搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
方法三:动态规划
<a href="#3" class="md-nav__link">
3. &nbsp; 方法三:动态规划
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
状态压缩
<a href="#4" class="md-nav__link">
4. &nbsp; 状态压缩
</a>
</li>
@@ -3397,29 +3397,29 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
方法一:暴力搜索
<a href="#1" class="md-nav__link">
1. &nbsp; 方法一:暴力搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
方法二:记忆化搜索
<a href="#2" class="md-nav__link">
2. &nbsp; 方法二:记忆化搜索
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
方法三:动态规划
<a href="#3" class="md-nav__link">
3. &nbsp; 方法三:动态规划
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
状态压缩
<a href="#4" class="md-nav__link">
4. &nbsp; 状态压缩
</a>
</li>
@@ -3479,7 +3479,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
<p>当无物品或无剩余背包容量时最大价值为 <span class="arithmatex">\(0\)</span> ,即首列 <span class="arithmatex">\(dp[i, 0]\)</span> 和首行 <span class="arithmatex">\(dp[0, c]\)</span> 都等于 <span class="arithmatex">\(0\)</span></p>
<p>当前状态 <span class="arithmatex">\([i, c]\)</span> 从上方的状态 <span class="arithmatex">\([i-1, c]\)</span> 和左上方的状态 <span class="arithmatex">\([i-1, c-wgt[i-1]]\)</span> 转移而来,因此通过两层循环正序遍历整个 <span class="arithmatex">\(dp\)</span> 表即可。</p>
<p>根据以上分析,我们接下来按顺序实现暴力搜索、记忆化搜索、动态规划解法。</p>
<h3 id="_1">方法一:暴力搜索<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<h3 id="1">1. &nbsp; 方法一:暴力搜索<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>搜索代码包含以下要素:</p>
<ul>
<li><strong>递归参数</strong>:状态 <span class="arithmatex">\([i, c]\)</span></li>
@@ -3676,7 +3676,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
<p><img alt="0-1 背包的暴力搜索递归树" src="../knapsack_problem.assets/knapsack_dfs.png" /></p>
<p align="center"> 图:0-1 背包的暴力搜索递归树 </p>
<h3 id="_2">方法二:记忆化搜索<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<h3 id="2">2. &nbsp; 方法二:记忆化搜索<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>为了保证重叠子问题只被计算一次,我们借助记忆列表 <code>mem</code> 来记录子问题的解,其中 <code>mem[i][c]</code> 对应 <span class="arithmatex">\(dp[i, c]\)</span></p>
<p>引入记忆化之后,<strong>时间复杂度取决于子问题数量</strong>,也就是 <span class="arithmatex">\(O(n \times cap)\)</span></p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:12"><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" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JS</label><label for="__tabbed_2_6">TS</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label><label for="__tabbed_2_11">Dart</label><label for="__tabbed_2_12">Rust</label></div>
@@ -3918,7 +3918,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
<p><img alt="0-1 背包的记忆化搜索递归树" src="../knapsack_problem.assets/knapsack_dfs_mem.png" /></p>
<p align="center"> 图:0-1 背包的记忆化搜索递归树 </p>
<h3 id="_3">方法三:动态规划<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<h3 id="3">3. &nbsp; 方法三:动态规划<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>动态规划实质上就是在状态转移中填充 <span class="arithmatex">\(dp\)</span> 表的过程,代码如下所示。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:12"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JS</label><label for="__tabbed_3_6">TS</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label><label for="__tabbed_3_11">Dart</label><label for="__tabbed_3_12">Rust</label></div>
<div class="tabbed-content">
@@ -4182,7 +4182,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
</div>
<p align="center"> 图:0-1 背包的动态规划过程 </p>
<h3 id="_4">状态压缩<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h3>
<h3 id="4">4. &nbsp; 状态压缩<a class="headerlink" href="#4" title="Permanent link">&para;</a></h3>
<p>由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 <span class="arithmatex">\(O(n^2)\)</span> 将低至 <span class="arithmatex">\(O(n)\)</span></p>
<p>进一步思考,我们是否可以仅用一个数组实现状态压缩呢?观察可知,每个状态都是由正上方或左上方的格子转移过来的。假设只有一个数组,当开始遍历第 <span class="arithmatex">\(i\)</span> 行时,该数组存储的仍然是第 <span class="arithmatex">\(i-1\)</span> 行的状态。</p>
<ul>
@@ -2906,15 +2906,22 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
代码实现
<a href="#1" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
状态压缩
<a href="#2" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -2933,15 +2940,22 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
代码实现
<a href="#1_1" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
状态压缩
<a href="#2_1" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3_1" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -2960,15 +2974,22 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_5" class="md-nav__link">
代码实现
<a href="#1_2" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_6" class="md-nav__link">
状态压缩
<a href="#2_2" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3_2" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -3458,15 +3479,22 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
代码实现
<a href="#1" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
状态压缩
<a href="#2" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -3485,15 +3513,22 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
代码实现
<a href="#1_1" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
状态压缩
<a href="#2_1" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3_1" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -3512,15 +3547,22 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_5" class="md-nav__link">
代码实现
<a href="#1_2" class="md-nav__link">
1. &nbsp; 动态规划思路
</a>
</li>
<li class="md-nav__item">
<a href="#_6" class="md-nav__link">
状态压缩
<a href="#2_2" class="md-nav__link">
2. &nbsp; 代码实现
</a>
</li>
<li class="md-nav__item">
<a href="#3_2" class="md-nav__link">
3. &nbsp; 状态压缩
</a>
</li>
@@ -3563,6 +3605,7 @@
<p><img alt="完全背包问题的示例数据" src="../unbounded_knapsack_problem.assets/unbounded_knapsack_example.png" /></p>
<p align="center"> 图:完全背包问题的示例数据 </p>
<h3 id="1">1. &nbsp; 动态规划思路<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>完全背包和 0-1 背包问题非常相似,<strong>区别仅在于不限制物品的选择次数</strong></p>
<ul>
<li>在 0-1 背包中,每个物品只有一个,因此将物品 <span class="arithmatex">\(i\)</span> 放入背包后,只能从前 <span class="arithmatex">\(i-1\)</span> 个物品中选择。</li>
@@ -3577,7 +3620,7 @@
<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>
<h3 id="2">2. &nbsp; 代码实现<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>对比两道题目的代码,状态转移中有一处从 <span class="arithmatex">\(i-1\)</span> 变为 <span class="arithmatex">\(i\)</span> ,其余完全一致。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><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" /><input id="__tabbed_1_12" 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">JS</label><label for="__tabbed_1_6">TS</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><label for="__tabbed_1_12">Rust</label></div>
<div class="tabbed-content">
@@ -3792,7 +3835,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
</div>
</div>
</div>
<h3 id="_2">状态压缩<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<h3 id="3">3. &nbsp; 状态压缩<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<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>
@@ -4040,6 +4083,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
<p><img alt="零钱兑换问题的示例数据" src="../unbounded_knapsack_problem.assets/coin_change_example.png" /></p>
<p align="center"> 图:零钱兑换问题的示例数据 </p>
<h3 id="1_1">1. &nbsp; 动态规划思路<a class="headerlink" href="#1_1" title="Permanent link">&para;</a></h3>
<p><strong>零钱兑换可以看作是完全背包的一种特殊情况</strong>,两者具有以下联系与不同点:</p>
<ul>
<li>两道题可以相互转换,“物品”对应于“硬币”、“物品重量”对应于“硬币面值”、“背包容量”对应于“目标金额”。</li>
@@ -4061,7 +4105,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
<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></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>
<h3 id="2_1">2. &nbsp; 代码实现<a class="headerlink" href="#2_1" title="Permanent link">&para;</a></h3>
<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>
@@ -4381,7 +4425,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
</div>
<p align="center"> 图:零钱兑换问题的动态规划过程 </p>
<h3 id="_4">状态压缩<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h3>
<h3 id="3_1">3. &nbsp; 状态压缩<a class="headerlink" href="#3_1" title="Permanent link">&para;</a></h3>
<p>零钱兑换的状态压缩的处理方式和完全背包一致。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="6:12"><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" /><input id="__tabbed_6_12" 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">JS</label><label for="__tabbed_6_6">TS</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><label for="__tabbed_6_12">Rust</label></div>
<div class="tabbed-content">
@@ -4634,13 +4678,14 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
<p><img alt="零钱兑换问题 II 的示例数据" src="../unbounded_knapsack_problem.assets/coin_change_ii_example.png" /></p>
<p align="center"> 图:零钱兑换问题 II 的示例数据 </p>
<h3 id="1_2">1. &nbsp; 动态规划思路<a class="headerlink" href="#1_2" title="Permanent link">&para;</a></h3>
<p>相比于上一题,本题目标是组合数量,因此子问题变为:<strong><span class="arithmatex">\(i\)</span> 种硬币能够凑出金额 <span class="arithmatex">\(a\)</span> 的组合数量</strong>。而 <span class="arithmatex">\(dp\)</span> 表仍然是尺寸为 <span class="arithmatex">\((n+1) \times (amt + 1)\)</span> 的二维矩阵。</p>
<p>当前状态的组合数量等于不选当前硬币与选当前硬币这两种决策的组合数量之和。状态转移方程为:</p>
<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>
<h3 id="_5">代码实现<a class="headerlink" href="#_5" title="Permanent link">&para;</a></h3>
<h3 id="2_2">2. &nbsp; 代码实现<a class="headerlink" href="#2_2" title="Permanent link">&para;</a></h3>
<div class="tabbed-set tabbed-alternate" data-tabs="7:12"><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" /><input id="__tabbed_7_12" 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">JS</label><label for="__tabbed_7_6">TS</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><label for="__tabbed_7_12">Rust</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4889,7 +4934,7 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
</div>
</div>
</div>
<h3 id="_6">状态压缩<a class="headerlink" href="#_6" title="Permanent link">&para;</a></h3>
<h3 id="3_2">3. &nbsp; 状态压缩<a class="headerlink" href="#3_2" title="Permanent link">&para;</a></h3>
<p>状态压缩处理方式相同,删除硬币维度即可。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="8:12"><input checked="checked" id="__tabbed_8_1" name="__tabbed_8" type="radio" /><input id="__tabbed_8_2" name="__tabbed_8" type="radio" /><input id="__tabbed_8_3" name="__tabbed_8" type="radio" /><input id="__tabbed_8_4" name="__tabbed_8" type="radio" /><input id="__tabbed_8_5" name="__tabbed_8" type="radio" /><input id="__tabbed_8_6" name="__tabbed_8" type="radio" /><input id="__tabbed_8_7" name="__tabbed_8" type="radio" /><input id="__tabbed_8_8" name="__tabbed_8" type="radio" /><input id="__tabbed_8_9" name="__tabbed_8" type="radio" /><input id="__tabbed_8_10" name="__tabbed_8" type="radio" /><input id="__tabbed_8_11" name="__tabbed_8" type="radio" /><input id="__tabbed_8_12" name="__tabbed_8" type="radio" /><div class="tabbed-labels"><label for="__tabbed_8_1">Java</label><label for="__tabbed_8_2">C++</label><label for="__tabbed_8_3">Python</label><label for="__tabbed_8_4">Go</label><label for="__tabbed_8_5">JS</label><label for="__tabbed_8_6">TS</label><label for="__tabbed_8_7">C</label><label for="__tabbed_8_8">C#</label><label for="__tabbed_8_9">Swift</label><label for="__tabbed_8_10">Zig</label><label for="__tabbed_8_11">Dart</label><label for="__tabbed_8_12">Rust</label></div>
<div class="tabbed-content">