mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-15 02:40:58 +08:00
deploy
This commit is contained in:
@@ -3433,9 +3433,9 @@
|
||||
<p class="admonition-title">爬楼梯最小代价</p>
|
||||
<p>给定一个楼梯,你每步可以上 <span class="arithmatex">\(1\)</span> 阶或者 <span class="arithmatex">\(2\)</span> 阶,每一阶楼梯上都贴有一个非负整数,表示你在该台阶所需要付出的代价。给定一个非负整数数组 <span class="arithmatex">\(cost\)</span> ,其中 <span class="arithmatex">\(cost[i]\)</span> 表示在第 <span class="arithmatex">\(i\)</span> 个台阶需要付出的代价,<span class="arithmatex">\(cost[0]\)</span> 为地面起始点。请计算最少需要付出多少代价才能到达顶部?</p>
|
||||
</div>
|
||||
<p>如下图所示,若第 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(3\)</span> 阶的代价分别为 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(10\)</span> , <span class="arithmatex">\(1\)</span> ,则从地面爬到第 <span class="arithmatex">\(3\)</span> 阶的最小代价为 <span class="arithmatex">\(2\)</span> 。</p>
|
||||
<p>如图 14-6 所示,若第 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(3\)</span> 阶的代价分别为 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(10\)</span> , <span class="arithmatex">\(1\)</span> ,则从地面爬到第 <span class="arithmatex">\(3\)</span> 阶的最小代价为 <span class="arithmatex">\(2\)</span> 。</p>
|
||||
<p><img alt="爬到第 3 阶的最小代价" src="../dp_problem_features.assets/min_cost_cs_example.png" /></p>
|
||||
<p align="center"> 图:爬到第 3 阶的最小代价 </p>
|
||||
<p align="center"> 图 14-6 爬到第 3 阶的最小代价 </p>
|
||||
|
||||
<p>设 <span class="arithmatex">\(dp[i]\)</span> 为爬到第 <span class="arithmatex">\(i\)</span> 阶累计付出的代价,由于第 <span class="arithmatex">\(i\)</span> 阶只可能从 <span class="arithmatex">\(i - 1\)</span> 阶或 <span class="arithmatex">\(i - 2\)</span> 阶走来,因此 <span class="arithmatex">\(dp[i]\)</span> 只可能等于 <span class="arithmatex">\(dp[i - 1] + cost[i]\)</span> 或 <span class="arithmatex">\(dp[i - 2] + cost[i]\)</span> 。为了尽可能减少代价,我们应该选择两者中较小的那一个,即:</p>
|
||||
<div class="arithmatex">\[
|
||||
@@ -3630,9 +3630,9 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图展示了以上代码的动态规划过程。</p>
|
||||
<p>图 14-7 展示了以上代码的动态规划过程。</p>
|
||||
<p><img alt="爬楼梯最小代价的动态规划过程" src="../dp_problem_features.assets/min_cost_cs_dp.png" /></p>
|
||||
<p align="center"> 图:爬楼梯最小代价的动态规划过程 </p>
|
||||
<p align="center"> 图 14-7 爬楼梯最小代价的动态规划过程 </p>
|
||||
|
||||
<p>本题也可以进行状态压缩,将一维压缩至零维,使得空间复杂度从 <span class="arithmatex">\(O(n)\)</span> 降低至 <span class="arithmatex">\(O(1)\)</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>
|
||||
@@ -3802,9 +3802,9 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
<p class="admonition-title">带约束爬楼梯</p>
|
||||
<p>给定一个共有 <span class="arithmatex">\(n\)</span> 阶的楼梯,你每步可以上 <span class="arithmatex">\(1\)</span> 阶或者 <span class="arithmatex">\(2\)</span> 阶,<strong>但不能连续两轮跳 <span class="arithmatex">\(1\)</span> 阶</strong>,请问有多少种方案可以爬到楼顶。</p>
|
||||
</div>
|
||||
<p>例如下图,爬上第 <span class="arithmatex">\(3\)</span> 阶仅剩 <span class="arithmatex">\(2\)</span> 种可行方案,其中连续三次跳 <span class="arithmatex">\(1\)</span> 阶的方案不满足约束条件,因此被舍弃。</p>
|
||||
<p>例如图 14-8 ,爬上第 <span class="arithmatex">\(3\)</span> 阶仅剩 <span class="arithmatex">\(2\)</span> 种可行方案,其中连续三次跳 <span class="arithmatex">\(1\)</span> 阶的方案不满足约束条件,因此被舍弃。</p>
|
||||
<p><img alt="带约束爬到第 3 阶的方案数量" src="../dp_problem_features.assets/climbing_stairs_constraint_example.png" /></p>
|
||||
<p align="center"> 图:带约束爬到第 3 阶的方案数量 </p>
|
||||
<p align="center"> 图 14-8 带约束爬到第 3 阶的方案数量 </p>
|
||||
|
||||
<p>在该问题中,如果上一轮是跳 <span class="arithmatex">\(1\)</span> 阶上来的,那么下一轮就必须跳 <span class="arithmatex">\(2\)</span> 阶。这意味着,<strong>下一步选择不能由当前状态(当前楼梯阶数)独立决定,还和前一个状态(上轮楼梯阶数)有关</strong>。</p>
|
||||
<p>不难发现,此问题已不满足无后效性,状态转移方程 <span class="arithmatex">\(dp[i] = dp[i-1] + dp[i-2]\)</span> 也失效了,因为 <span class="arithmatex">\(dp[i-1]\)</span> 代表本轮跳 <span class="arithmatex">\(1\)</span> 阶,但其中包含了许多“上一轮跳 <span class="arithmatex">\(1\)</span> 阶上来的”方案,而为了满足约束,我们就不能将 <span class="arithmatex">\(dp[i-1]\)</span> 直接计入 <span class="arithmatex">\(dp[i]\)</span> 中。</p>
|
||||
@@ -3813,7 +3813,7 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
<li>当 <span class="arithmatex">\(j\)</span> 等于 <span class="arithmatex">\(1\)</span> ,即上一轮跳了 <span class="arithmatex">\(1\)</span> 阶时,这一轮只能选择跳 <span class="arithmatex">\(2\)</span> 阶。</li>
|
||||
<li>当 <span class="arithmatex">\(j\)</span> 等于 <span class="arithmatex">\(2\)</span> ,即上一轮跳了 <span class="arithmatex">\(2\)</span> 阶时,这一轮可选择跳 <span class="arithmatex">\(1\)</span> 阶或跳 <span class="arithmatex">\(2\)</span> 阶。</li>
|
||||
</ul>
|
||||
<p>如下图所示,在该定义下,<span class="arithmatex">\(dp[i, j]\)</span> 表示状态 <span class="arithmatex">\([i, j]\)</span> 对应的方案数。此时状态转移方程为:</p>
|
||||
<p>如图 14-9 所示,在该定义下,<span class="arithmatex">\(dp[i, j]\)</span> 表示状态 <span class="arithmatex">\([i, j]\)</span> 对应的方案数。此时状态转移方程为:</p>
|
||||
<div class="arithmatex">\[
|
||||
\begin{cases}
|
||||
dp[i, 1] = dp[i-1, 2] \\
|
||||
@@ -3821,7 +3821,7 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
|
||||
\end{cases}
|
||||
\]</div>
|
||||
<p><img alt="考虑约束下的递推关系" src="../dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png" /></p>
|
||||
<p align="center"> 图:考虑约束下的递推关系 </p>
|
||||
<p align="center"> 图 14-9 考虑约束下的递推关系 </p>
|
||||
|
||||
<p>最终,返回 <span class="arithmatex">\(dp[n, 1] + dp[n, 2]\)</span> 即可,两者之和代表爬到第 <span class="arithmatex">\(n\)</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>
|
||||
|
||||
@@ -3515,16 +3515,16 @@
|
||||
<p class="admonition-title">Question</p>
|
||||
<p>给定一个 <span class="arithmatex">\(n \times m\)</span> 的二维网格 <code>grid</code> ,网格中的每个单元格包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回从左上角到右下角的最小路径和。</p>
|
||||
</div>
|
||||
<p>下图展示了一个例子,给定网格的最小路径和为 <span class="arithmatex">\(13\)</span> 。</p>
|
||||
<p>图 14-10 展示了一个例子,给定网格的最小路径和为 <span class="arithmatex">\(13\)</span> 。</p>
|
||||
<p><img alt="最小路径和示例数据" src="../dp_solution_pipeline.assets/min_path_sum_example.png" /></p>
|
||||
<p align="center"> 图:最小路径和示例数据 </p>
|
||||
<p align="center"> 图 14-10 最小路径和示例数据 </p>
|
||||
|
||||
<p><strong>第一步:思考每轮的决策,定义状态,从而得到 <span class="arithmatex">\(dp\)</span> 表</strong></p>
|
||||
<p>本题的每一轮的决策就是从当前格子向下或向右一步。设当前格子的行列索引为 <span class="arithmatex">\([i, j]\)</span> ,则向下或向右走一步后,索引变为 <span class="arithmatex">\([i+1, j]\)</span> 或 <span class="arithmatex">\([i, j+1]\)</span> 。因此,状态应包含行索引和列索引两个变量,记为 <span class="arithmatex">\([i, j]\)</span> 。</p>
|
||||
<p>状态 <span class="arithmatex">\([i, j]\)</span> 对应的子问题为:从起始点 <span class="arithmatex">\([0, 0]\)</span> 走到 <span class="arithmatex">\([i, j]\)</span> 的最小路径和,解记为 <span class="arithmatex">\(dp[i, j]\)</span> 。</p>
|
||||
<p>至此,我们就得到了下图所示的二维 <span class="arithmatex">\(dp\)</span> 矩阵,其尺寸与输入网格 <span class="arithmatex">\(grid\)</span> 相同。</p>
|
||||
<p>至此,我们就得到了图 14-11 所示的二维 <span class="arithmatex">\(dp\)</span> 矩阵,其尺寸与输入网格 <span class="arithmatex">\(grid\)</span> 相同。</p>
|
||||
<p><img alt="状态定义与 dp 表" src="../dp_solution_pipeline.assets/min_path_sum_solution_step1.png" /></p>
|
||||
<p align="center"> 图:状态定义与 dp 表 </p>
|
||||
<p align="center"> 图 14-11 状态定义与 dp 表 </p>
|
||||
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
@@ -3533,12 +3533,12 @@
|
||||
</div>
|
||||
<p><strong>第二步:找出最优子结构,进而推导出状态转移方程</strong></p>
|
||||
<p>对于状态 <span class="arithmatex">\([i, j]\)</span> ,它只能从上边格子 <span class="arithmatex">\([i-1, j]\)</span> 和左边格子 <span class="arithmatex">\([i, j-1]\)</span> 转移而来。因此最优子结构为:到达 <span class="arithmatex">\([i, j]\)</span> 的最小路径和由 <span class="arithmatex">\([i, j-1]\)</span> 的最小路径和与 <span class="arithmatex">\([i-1, j]\)</span> 的最小路径和,这两者较小的那一个决定。</p>
|
||||
<p>根据以上分析,可推出下图所示的状态转移方程:</p>
|
||||
<p>根据以上分析,可推出图 14-12 所示的状态转移方程:</p>
|
||||
<div class="arithmatex">\[
|
||||
dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
\]</div>
|
||||
<p><img alt="最优子结构与状态转移方程" src="../dp_solution_pipeline.assets/min_path_sum_solution_step2.png" /></p>
|
||||
<p align="center"> 图:最优子结构与状态转移方程 </p>
|
||||
<p align="center"> 图 14-12 最优子结构与状态转移方程 </p>
|
||||
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
@@ -3547,9 +3547,9 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
</div>
|
||||
<p><strong>第三步:确定边界条件和状态转移顺序</strong></p>
|
||||
<p>在本题中,处在首行的状态只能向右转移,首列状态只能向下转移,因此首行 <span class="arithmatex">\(i = 0\)</span> 和首列 <span class="arithmatex">\(j = 0\)</span> 是边界条件。</p>
|
||||
<p>如下图所示,由于每个格子是由其左方格子和上方格子转移而来,因此我们使用采用循环来遍历矩阵,外循环遍历各行、内循环遍历各列。</p>
|
||||
<p>如图 14-13 所示,由于每个格子是由其左方格子和上方格子转移而来,因此我们使用采用循环来遍历矩阵,外循环遍历各行、内循环遍历各列。</p>
|
||||
<p><img alt="边界条件与状态转移顺序" src="../dp_solution_pipeline.assets/min_path_sum_solution_step3.png" /></p>
|
||||
<p align="center"> 图:边界条件与状态转移顺序 </p>
|
||||
<p align="center"> 图 14-13 边界条件与状态转移顺序 </p>
|
||||
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
@@ -3750,10 +3750,10 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图给出了以 <span class="arithmatex">\(dp[2, 1]\)</span> 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 <code>grid</code> 的尺寸变大而急剧增多。</p>
|
||||
<p>图 14-14 给出了以 <span class="arithmatex">\(dp[2, 1]\)</span> 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 <code>grid</code> 的尺寸变大而急剧增多。</p>
|
||||
<p>本质上看,造成重叠子问题的原因为:<strong>存在多条路径可以从左上角到达某一单元格</strong>。</p>
|
||||
<p><img alt="暴力搜索递归树" src="../dp_solution_pipeline.assets/min_path_sum_dfs.png" /></p>
|
||||
<p align="center"> 图:暴力搜索递归树 </p>
|
||||
<p align="center"> 图 14-14 暴力搜索递归树 </p>
|
||||
|
||||
<p>每个状态都有向下和向右两种选择,从左上角走到右下角总共需要 <span class="arithmatex">\(m + n - 2\)</span> 步,所以最差时间复杂度为 <span class="arithmatex">\(O(2^{m + n})\)</span> 。请注意,这种计算方式未考虑临近网格边界的情况,当到达网络边界时只剩下一种选择。因此实际的路径数量会少一些。</p>
|
||||
<h3 id="2">2. 方法二:记忆化搜索<a class="headerlink" href="#2" title="Permanent link">¶</a></h3>
|
||||
@@ -3990,9 +3990,9 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>如下图所示,在引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 <span class="arithmatex">\(O(nm)\)</span> 。</p>
|
||||
<p>如图 14-15 所示,在引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 <span class="arithmatex">\(O(nm)\)</span> 。</p>
|
||||
<p><img alt="记忆化搜索递归树" src="../dp_solution_pipeline.assets/min_path_sum_dfs_mem.png" /></p>
|
||||
<p align="center"> 图:记忆化搜索递归树 </p>
|
||||
<p align="center"> 图 14-15 记忆化搜索递归树 </p>
|
||||
|
||||
<h3 id="3">3. 方法三:动态规划<a class="headerlink" href="#3" title="Permanent link">¶</a></h3>
|
||||
<p>基于迭代实现动态规划解法。</p>
|
||||
@@ -4237,7 +4237,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图展示了最小路径和的状态转移过程,其遍历了整个网格,<strong>因此时间复杂度为 <span class="arithmatex">\(O(nm)\)</span></strong> 。</p>
|
||||
<p>图 14-16 展示了最小路径和的状态转移过程,其遍历了整个网格,<strong>因此时间复杂度为 <span class="arithmatex">\(O(nm)\)</span></strong> 。</p>
|
||||
<p>数组 <code>dp</code> 大小为 <span class="arithmatex">\(n \times m\)</span> ,<strong>因此空间复杂度为 <span class="arithmatex">\(O(nm)\)</span></strong> 。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="4:12"><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" /><input id="__tabbed_4_12" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1"><1></label><label for="__tabbed_4_2"><2></label><label for="__tabbed_4_3"><3></label><label for="__tabbed_4_4"><4></label><label for="__tabbed_4_5"><5></label><label for="__tabbed_4_6"><6></label><label for="__tabbed_4_7"><7></label><label for="__tabbed_4_8"><8></label><label for="__tabbed_4_9"><9></label><label for="__tabbed_4_10"><10></label><label for="__tabbed_4_11"><11></label><label for="__tabbed_4_12"><12></label></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -4279,7 +4279,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p align="center"> 图:最小路径和的动态规划过程 </p>
|
||||
<p align="center"> 图 14-16 最小路径和的动态规划过程 </p>
|
||||
|
||||
<h3 id="4">4. 状态压缩<a class="headerlink" href="#4" title="Permanent link">¶</a></h3>
|
||||
<p>由于每个格子只与其左边和上边的格子有关,因此我们可以只用一个单行数组来实现 <span class="arithmatex">\(dp\)</span> 表。</p>
|
||||
|
||||
@@ -3440,15 +3440,15 @@
|
||||
<p>输入两个字符串 <span class="arithmatex">\(s\)</span> 和 <span class="arithmatex">\(t\)</span> ,返回将 <span class="arithmatex">\(s\)</span> 转换为 <span class="arithmatex">\(t\)</span> 所需的最少编辑步数。</p>
|
||||
<p>你可以在一个字符串中进行三种编辑操作:插入一个字符、删除一个字符、替换字符为任意一个字符。</p>
|
||||
</div>
|
||||
<p>如下图所示,将 <code>kitten</code> 转换为 <code>sitting</code> 需要编辑 3 步,包括 2 次替换操作与 1 次添加操作;将 <code>hello</code> 转换为 <code>algo</code> 需要 3 步,包括 2 次替换操作和 1 次删除操作。</p>
|
||||
<p>如图 14-27 所示,将 <code>kitten</code> 转换为 <code>sitting</code> 需要编辑 3 步,包括 2 次替换操作与 1 次添加操作;将 <code>hello</code> 转换为 <code>algo</code> 需要 3 步,包括 2 次替换操作和 1 次删除操作。</p>
|
||||
<p><img alt="编辑距离的示例数据" src="../edit_distance_problem.assets/edit_distance_example.png" /></p>
|
||||
<p align="center"> 图:编辑距离的示例数据 </p>
|
||||
<p align="center"> 图 14-27 编辑距离的示例数据 </p>
|
||||
|
||||
<p><strong>编辑距离问题可以很自然地用决策树模型来解释</strong>。字符串对应树节点,一轮决策(一次编辑操作)对应树的一条边。</p>
|
||||
<p>如下图所示,在不限制操作的情况下,每个节点都可以派生出许多条边,每条边对应一种操作,这意味着从 <code>hello</code> 转换到 <code>algo</code> 有许多种可能的路径。</p>
|
||||
<p>如图 14-28 所示,在不限制操作的情况下,每个节点都可以派生出许多条边,每条边对应一种操作,这意味着从 <code>hello</code> 转换到 <code>algo</code> 有许多种可能的路径。</p>
|
||||
<p>从决策树的角度看,本题的目标是求解节点 <code>hello</code> 和节点 <code>algo</code> 之间的最短路径。</p>
|
||||
<p><img alt="基于决策树模型表示编辑距离问题" src="../edit_distance_problem.assets/edit_distance_decision_tree.png" /></p>
|
||||
<p align="center"> 图:基于决策树模型表示编辑距离问题 </p>
|
||||
<p align="center"> 图 14-28 基于决策树模型表示编辑距离问题 </p>
|
||||
|
||||
<h3 id="1">1. 动态规划思路<a class="headerlink" href="#1" title="Permanent link">¶</a></h3>
|
||||
<p><strong>第一步:思考每轮的决策,定义状态,从而得到 <span class="arithmatex">\(dp\)</span> 表</strong></p>
|
||||
@@ -3462,14 +3462,14 @@
|
||||
<p>状态 <span class="arithmatex">\([i, j]\)</span> 对应的子问题:<strong>将 <span class="arithmatex">\(s\)</span> 的前 <span class="arithmatex">\(i\)</span> 个字符更改为 <span class="arithmatex">\(t\)</span> 的前 <span class="arithmatex">\(j\)</span> 个字符所需的最少编辑步数</strong>。</p>
|
||||
<p>至此,得到一个尺寸为 <span class="arithmatex">\((i+1) \times (j+1)\)</span> 的二维 <span class="arithmatex">\(dp\)</span> 表。</p>
|
||||
<p><strong>第二步:找出最优子结构,进而推导出状态转移方程</strong></p>
|
||||
<p>考虑子问题 <span class="arithmatex">\(dp[i, j]\)</span> ,其对应的两个字符串的尾部字符为 <span class="arithmatex">\(s[i-1]\)</span> 和 <span class="arithmatex">\(t[j-1]\)</span> ,可根据不同编辑操作分为下图所示的三种情况:</p>
|
||||
<p>考虑子问题 <span class="arithmatex">\(dp[i, j]\)</span> ,其对应的两个字符串的尾部字符为 <span class="arithmatex">\(s[i-1]\)</span> 和 <span class="arithmatex">\(t[j-1]\)</span> ,可根据不同编辑操作分为图 14-29 所示的三种情况:</p>
|
||||
<ol>
|
||||
<li>在 <span class="arithmatex">\(s[i-1]\)</span> 之后添加 <span class="arithmatex">\(t[j-1]\)</span> ,则剩余子问题 <span class="arithmatex">\(dp[i, j-1]\)</span> 。</li>
|
||||
<li>删除 <span class="arithmatex">\(s[i-1]\)</span> ,则剩余子问题 <span class="arithmatex">\(dp[i-1, j]\)</span> 。</li>
|
||||
<li>将 <span class="arithmatex">\(s[i-1]\)</span> 替换为 <span class="arithmatex">\(t[j-1]\)</span> ,则剩余子问题 <span class="arithmatex">\(dp[i-1, j-1]\)</span> 。</li>
|
||||
</ol>
|
||||
<p><img alt="编辑距离的状态转移" src="../edit_distance_problem.assets/edit_distance_state_transfer.png" /></p>
|
||||
<p align="center"> 图:编辑距离的状态转移 </p>
|
||||
<p align="center"> 图 14-29 编辑距离的状态转移 </p>
|
||||
|
||||
<p>根据以上分析,可得最优子结构:<span class="arithmatex">\(dp[i, j]\)</span> 的最少编辑步数等于 <span class="arithmatex">\(dp[i, j-1]\)</span> , <span class="arithmatex">\(dp[i-1, j]\)</span> , <span class="arithmatex">\(dp[i-1, j-1]\)</span> 三者中的最少编辑步数,再加上本次的编辑步数 <span class="arithmatex">\(1\)</span> 。对应的状态转移方程为:</p>
|
||||
<div class="arithmatex">\[
|
||||
@@ -3751,7 +3751,7 @@ dp[i, j] = dp[i-1, j-1]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>如下图所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作是填写一个二维网格的过程。</p>
|
||||
<p>如图 14-30 所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作是填写一个二维网格的过程。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="2:15"><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" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><input id="__tabbed_2_14" name="__tabbed_2" type="radio" /><input id="__tabbed_2_15" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1"><1></label><label for="__tabbed_2_2"><2></label><label for="__tabbed_2_3"><3></label><label for="__tabbed_2_4"><4></label><label for="__tabbed_2_5"><5></label><label for="__tabbed_2_6"><6></label><label for="__tabbed_2_7"><7></label><label for="__tabbed_2_8"><8></label><label for="__tabbed_2_9"><9></label><label for="__tabbed_2_10"><10></label><label for="__tabbed_2_11"><11></label><label for="__tabbed_2_12"><12></label><label for="__tabbed_2_13"><13></label><label for="__tabbed_2_14"><14></label><label for="__tabbed_2_15"><15></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3801,7 +3801,7 @@ dp[i, j] = dp[i-1, j-1]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p align="center"> 图:编辑距离的动态规划过程 </p>
|
||||
<p align="center"> 图 14-30 编辑距离的动态规划过程 </p>
|
||||
|
||||
<h3 id="3">3. 状态压缩<a class="headerlink" href="#3" title="Permanent link">¶</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>
|
||||
|
||||
@@ -3454,9 +3454,9 @@
|
||||
<p class="admonition-title">爬楼梯</p>
|
||||
<p>给定一个共有 <span class="arithmatex">\(n\)</span> 阶的楼梯,你每步可以上 <span class="arithmatex">\(1\)</span> 阶或者 <span class="arithmatex">\(2\)</span> 阶,请问有多少种方案可以爬到楼顶。</p>
|
||||
</div>
|
||||
<p>如下图所示,对于一个 <span class="arithmatex">\(3\)</span> 阶楼梯,共有 <span class="arithmatex">\(3\)</span> 种方案可以爬到楼顶。</p>
|
||||
<p>如图 14-1 所示,对于一个 <span class="arithmatex">\(3\)</span> 阶楼梯,共有 <span class="arithmatex">\(3\)</span> 种方案可以爬到楼顶。</p>
|
||||
<p><img alt="爬到第 3 阶的方案数量" src="../intro_to_dynamic_programming.assets/climbing_stairs_example.png" /></p>
|
||||
<p align="center"> 图:爬到第 3 阶的方案数量 </p>
|
||||
<p align="center"> 图 14-1 爬到第 3 阶的方案数量 </p>
|
||||
|
||||
<p>本题的目标是求解方案数量,<strong>我们可以考虑通过回溯来穷举所有可能性</strong>。具体来说,将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上 <span class="arithmatex">\(1\)</span> 阶或 <span class="arithmatex">\(2\)</span> 阶,每当到达楼梯顶部时就将方案数量加 <span class="arithmatex">\(1\)</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>
|
||||
@@ -3789,9 +3789,9 @@ dp[i-1] , dp[i-2] , \dots , dp[2] , dp[1]
|
||||
<div class="arithmatex">\[
|
||||
dp[i] = dp[i-1] + dp[i-2]
|
||||
\]</div>
|
||||
<p>这意味着在爬楼梯问题中,各个子问题之间存在递推关系,<strong>原问题的解可以由子问题的解构建得来</strong>。下图展示了该递推关系。</p>
|
||||
<p>这意味着在爬楼梯问题中,各个子问题之间存在递推关系,<strong>原问题的解可以由子问题的解构建得来</strong>。图 14-2 展示了该递推关系。</p>
|
||||
<p><img alt="方案数量递推关系" src="../intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png" /></p>
|
||||
<p align="center"> 图:方案数量递推关系 </p>
|
||||
<p align="center"> 图 14-2 方案数量递推关系 </p>
|
||||
|
||||
<p>我们可以根据递推公式得到暴力搜索解法:</p>
|
||||
<ul>
|
||||
@@ -3993,11 +3993,11 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图展示了暴力搜索形成的递归树。对于问题 <span class="arithmatex">\(dp[n]\)</span> ,其递归树的深度为 <span class="arithmatex">\(n\)</span> ,时间复杂度为 <span class="arithmatex">\(O(2^n)\)</span> 。指数阶属于爆炸式增长,如果我们输入一个比较大的 <span class="arithmatex">\(n\)</span> ,则会陷入漫长的等待之中。</p>
|
||||
<p>图 14-3 展示了暴力搜索形成的递归树。对于问题 <span class="arithmatex">\(dp[n]\)</span> ,其递归树的深度为 <span class="arithmatex">\(n\)</span> ,时间复杂度为 <span class="arithmatex">\(O(2^n)\)</span> 。指数阶属于爆炸式增长,如果我们输入一个比较大的 <span class="arithmatex">\(n\)</span> ,则会陷入漫长的等待之中。</p>
|
||||
<p><img alt="爬楼梯对应递归树" src="../intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png" /></p>
|
||||
<p align="center"> 图:爬楼梯对应递归树 </p>
|
||||
<p align="center"> 图 14-3 爬楼梯对应递归树 </p>
|
||||
|
||||
<p>观察上图发现,<strong>指数阶的时间复杂度是由于“重叠子问题”导致的</strong>。例如:<span class="arithmatex">\(dp[9]\)</span> 被分解为 <span class="arithmatex">\(dp[8]\)</span> 和 <span class="arithmatex">\(dp[7]\)</span> ,<span class="arithmatex">\(dp[8]\)</span> 被分解为 <span class="arithmatex">\(dp[7]\)</span> 和 <span class="arithmatex">\(dp[6]\)</span> ,两者都包含子问题 <span class="arithmatex">\(dp[7]\)</span> 。</p>
|
||||
<p>观察图 14-3 ,<strong>指数阶的时间复杂度是由于“重叠子问题”导致的</strong>。例如 <span class="arithmatex">\(dp[9]\)</span> 被分解为 <span class="arithmatex">\(dp[8]\)</span> 和 <span class="arithmatex">\(dp[7]\)</span> ,<span class="arithmatex">\(dp[8]\)</span> 被分解为 <span class="arithmatex">\(dp[7]\)</span> 和 <span class="arithmatex">\(dp[6]\)</span> ,两者都包含子问题 <span class="arithmatex">\(dp[7]\)</span> 。</p>
|
||||
<p>以此类推,子问题中包含更小的重叠子问题,子子孙孙无穷尽也。绝大部分计算资源都浪费在这些重叠的问题上。</p>
|
||||
<h2 id="1412">14.1.2 方法二:记忆化搜索<a class="headerlink" href="#1412" title="Permanent link">¶</a></h2>
|
||||
<p>为了提升算法效率,<strong>我们希望所有的重叠子问题都只被计算一次</strong>。为此,我们声明一个数组 <code>mem</code> 来记录每个子问题的解,并在搜索过程中这样做:</p>
|
||||
@@ -4280,9 +4280,9 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>观察下图,<strong>经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 <span class="arithmatex">\(O(n)\)</span></strong> ,这是一个巨大的飞跃。</p>
|
||||
<p>观察图 14-4 ,<strong>经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 <span class="arithmatex">\(O(n)\)</span></strong> ,这是一个巨大的飞跃。</p>
|
||||
<p><img alt="记忆化搜索对应递归树" src="../intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png" /></p>
|
||||
<p align="center"> 图:记忆化搜索对应递归树 </p>
|
||||
<p align="center"> 图 14-4 记忆化搜索对应递归树 </p>
|
||||
|
||||
<h2 id="1413">14.1.3 方法三:动态规划<a class="headerlink" href="#1413" title="Permanent link">¶</a></h2>
|
||||
<p><strong>记忆化搜索是一种“从顶至底”的方法</strong>:我们从原问题(根节点)开始,递归地将较大子问题分解为较小子问题,直至解已知的最小子问题(叶节点)。之后,通过回溯将子问题的解逐层收集,构建出原问题的解。</p>
|
||||
@@ -4492,9 +4492,9 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图模拟了以上代码的执行过程。</p>
|
||||
<p>图 14-5 模拟了以上代码的执行过程。</p>
|
||||
<p><img alt="爬楼梯的动态规划过程" src="../intro_to_dynamic_programming.assets/climbing_stairs_dp.png" /></p>
|
||||
<p align="center"> 图:爬楼梯的动态规划过程 </p>
|
||||
<p align="center"> 图 14-5 爬楼梯的动态规划过程 </p>
|
||||
|
||||
<p>与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的某个特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 <span class="arithmatex">\(i\)</span> 。</p>
|
||||
<p>总结以上,动态规划的常用术语包括:</p>
|
||||
|
||||
@@ -3454,9 +3454,9 @@
|
||||
<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> 的背包。每个物品只能选择一次,问在不超过背包容量下能放入物品的最大价值。</p>
|
||||
</div>
|
||||
<p>观察下图,由于物品编号 <span class="arithmatex">\(i\)</span> 从 <span class="arithmatex">\(1\)</span> 开始计数,数组索引从 <span class="arithmatex">\(0\)</span> 开始计数,因此物品 <span class="arithmatex">\(i\)</span> 对应重量 <span class="arithmatex">\(wgt[i-1]\)</span> 和价值 <span class="arithmatex">\(val[i-1]\)</span> 。</p>
|
||||
<p>观察图 14-17 ,由于物品编号 <span class="arithmatex">\(i\)</span> 从 <span class="arithmatex">\(1\)</span> 开始计数,数组索引从 <span class="arithmatex">\(0\)</span> 开始计数,因此物品 <span class="arithmatex">\(i\)</span> 对应重量 <span class="arithmatex">\(wgt[i-1]\)</span> 和价值 <span class="arithmatex">\(val[i-1]\)</span> 。</p>
|
||||
<p><img alt="0-1 背包的示例数据" src="../knapsack_problem.assets/knapsack_example.png" /></p>
|
||||
<p align="center"> 图:0-1 背包的示例数据 </p>
|
||||
<p align="center"> 图 14-17 0-1 背包的示例数据 </p>
|
||||
|
||||
<p>我们可以将 0-1 背包问题看作是一个由 <span class="arithmatex">\(n\)</span> 轮决策组成的过程,每个物体都有不放入和放入两种决策,因此该问题是满足决策树模型的。</p>
|
||||
<p>该问题的目标是求解“在限定背包容量下的最大价值”,因此较大概率是个动态规划问题。</p>
|
||||
@@ -3671,10 +3671,10 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>如下图所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 <span class="arithmatex">\(O(2^n)\)</span> 。</p>
|
||||
<p>如图 14-18 所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 <span class="arithmatex">\(O(2^n)\)</span> 。</p>
|
||||
<p>观察递归树,容易发现其中存在重叠子问题,例如 <span class="arithmatex">\(dp[1, 10]\)</span> 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。</p>
|
||||
<p><img alt="0-1 背包的暴力搜索递归树" src="../knapsack_problem.assets/knapsack_dfs.png" /></p>
|
||||
<p align="center"> 图:0-1 背包的暴力搜索递归树 </p>
|
||||
<p align="center"> 图 14-18 0-1 背包的暴力搜索递归树 </p>
|
||||
|
||||
<h3 id="2">2. 方法二:记忆化搜索<a class="headerlink" href="#2" title="Permanent link">¶</a></h3>
|
||||
<p>为了保证重叠子问题只被计算一次,我们借助记忆列表 <code>mem</code> 来记录子问题的解,其中 <code>mem[i][c]</code> 对应 <span class="arithmatex">\(dp[i, c]\)</span> 。</p>
|
||||
@@ -3915,9 +3915,9 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图展示了在记忆化递归中被剪掉的搜索分支。</p>
|
||||
<p>图 14-19 展示了在记忆化递归中被剪掉的搜索分支。</p>
|
||||
<p><img alt="0-1 背包的记忆化搜索递归树" src="../knapsack_problem.assets/knapsack_dfs_mem.png" /></p>
|
||||
<p align="center"> 图:0-1 背包的记忆化搜索递归树 </p>
|
||||
<p align="center"> 图 14-19 0-1 背包的记忆化搜索递归树 </p>
|
||||
|
||||
<h3 id="3">3. 方法三:动态规划<a class="headerlink" href="#3" title="Permanent link">¶</a></h3>
|
||||
<p>动态规划实质上就是在状态转移中填充 <span class="arithmatex">\(dp\)</span> 表的过程,代码如下所示。</p>
|
||||
@@ -4134,7 +4134,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>如下图所示,时间复杂度和空间复杂度都由数组 <code>dp</code> 大小决定,即 <span class="arithmatex">\(O(n \times cap)\)</span> 。</p>
|
||||
<p>如图 14-20 所示,时间复杂度和空间复杂度都由数组 <code>dp</code> 大小决定,即 <span class="arithmatex">\(O(n \times cap)\)</span> 。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="4:14"><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" /><input id="__tabbed_4_12" name="__tabbed_4" type="radio" /><input id="__tabbed_4_13" name="__tabbed_4" type="radio" /><input id="__tabbed_4_14" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1"><1></label><label for="__tabbed_4_2"><2></label><label for="__tabbed_4_3"><3></label><label for="__tabbed_4_4"><4></label><label for="__tabbed_4_5"><5></label><label for="__tabbed_4_6"><6></label><label for="__tabbed_4_7"><7></label><label for="__tabbed_4_8"><8></label><label for="__tabbed_4_9"><9></label><label for="__tabbed_4_10"><10></label><label for="__tabbed_4_11"><11></label><label for="__tabbed_4_12"><12></label><label for="__tabbed_4_13"><13></label><label for="__tabbed_4_14"><14></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -4181,7 +4181,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p align="center"> 图:0-1 背包的动态规划过程 </p>
|
||||
<p align="center"> 图 14-20 0-1 背包的动态规划过程 </p>
|
||||
|
||||
<h3 id="4">4. 状态压缩<a class="headerlink" href="#4" title="Permanent link">¶</a></h3>
|
||||
<p>由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 <span class="arithmatex">\(O(n^2)\)</span> 将低至 <span class="arithmatex">\(O(n)\)</span> 。</p>
|
||||
@@ -4190,7 +4190,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
<li>如果采取正序遍历,那么遍历到 <span class="arithmatex">\(dp[i, j]\)</span> 时,左上方 <span class="arithmatex">\(dp[i-1, 1]\)</span> ~ <span class="arithmatex">\(dp[i-1, j-1]\)</span> 值可能已经被覆盖,此时就无法得到正确的状态转移结果。</li>
|
||||
<li>如果采取倒序遍历,则不会发生覆盖问题,状态转移可以正确进行。</li>
|
||||
</ul>
|
||||
<p>下图展示了在单个数组下从第 <span class="arithmatex">\(i = 1\)</span> 行转换至第 <span class="arithmatex">\(i = 2\)</span> 行的过程。请思考正序遍历和倒序遍历的区别。</p>
|
||||
<p>图 14-21 展示了在单个数组下从第 <span class="arithmatex">\(i = 1\)</span> 行转换至第 <span class="arithmatex">\(i = 2\)</span> 行的过程。请思考正序遍历和倒序遍历的区别。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="5:6"><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" /><div class="tabbed-labels"><label for="__tabbed_5_1"><1></label><label for="__tabbed_5_2"><2></label><label for="__tabbed_5_3"><3></label><label for="__tabbed_5_4"><4></label><label for="__tabbed_5_5"><5></label><label for="__tabbed_5_6"><6></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -4213,7 +4213,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p align="center"> 图:0-1 背包的状态压缩后的动态规划过程 </p>
|
||||
<p align="center"> 图 14-21 0-1 背包的状态压缩后的动态规划过程 </p>
|
||||
|
||||
<p>在代码实现中,我们仅需将数组 <code>dp</code> 的第一维 <span class="arithmatex">\(i\)</span> 直接删除,并且把内循环更改为倒序遍历即可。</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>
|
||||
|
||||
@@ -3603,7 +3603,7 @@
|
||||
<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"> 图:完全背包问题的示例数据 </p>
|
||||
<p align="center"> 图 14-22 完全背包问题的示例数据 </p>
|
||||
|
||||
<h3 id="1">1. 动态规划思路<a class="headerlink" href="#1" title="Permanent link">¶</a></h3>
|
||||
<p>完全背包和 0-1 背包问题非常相似,<strong>区别仅在于不限制物品的选择次数</strong>。</p>
|
||||
@@ -3837,7 +3837,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
<h3 id="3">3. 状态压缩<a class="headerlink" href="#3" title="Permanent link">¶</a></h3>
|
||||
<p>由于当前状态是从左边和上边的状态转移而来,<strong>因此状态压缩后应该对 <span class="arithmatex">\(dp\)</span> 表中的每一行采取正序遍历</strong>。</p>
|
||||
<p>这个遍历顺序与 0-1 背包正好相反。请借助下图来理解两者的区别。</p>
|
||||
<p>这个遍历顺序与 0-1 背包正好相反。请借助图 14-23 来理解两者的区别。</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"><1></label><label for="__tabbed_2_2"><2></label><label for="__tabbed_2_3"><3></label><label for="__tabbed_2_4"><4></label><label for="__tabbed_2_5"><5></label><label for="__tabbed_2_6"><6></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3860,7 +3860,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p align="center"> 图:完全背包的状态压缩后的动态规划过程 </p>
|
||||
<p align="center"> 图 14-23 完全背包的状态压缩后的动态规划过程 </p>
|
||||
|
||||
<p>代码实现比较简单,仅需将数组 <code>dp</code> 的第一维删除。</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>
|
||||
@@ -4081,7 +4081,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
<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><img alt="零钱兑换问题的示例数据" src="../unbounded_knapsack_problem.assets/coin_change_example.png" /></p>
|
||||
<p align="center"> 图:零钱兑换问题的示例数据 </p>
|
||||
<p align="center"> 图 14-24 零钱兑换问题的示例数据 </p>
|
||||
|
||||
<h3 id="1_1">1. 动态规划思路<a class="headerlink" href="#1_1" title="Permanent link">¶</a></h3>
|
||||
<p><strong>零钱兑换可以看作是完全背包的一种特殊情况</strong>,两者具有以下联系与不同点:</p>
|
||||
@@ -4373,7 +4373,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下图展示了零钱兑换的动态规划过程,和完全背包非常相似。</p>
|
||||
<p>图 14-25 展示了零钱兑换的动态规划过程,和完全背包非常相似。</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"><1></label><label for="__tabbed_5_2"><2></label><label for="__tabbed_5_3"><3></label><label for="__tabbed_5_4"><4></label><label for="__tabbed_5_5"><5></label><label for="__tabbed_5_6"><6></label><label for="__tabbed_5_7"><7></label><label for="__tabbed_5_8"><8></label><label for="__tabbed_5_9"><9></label><label for="__tabbed_5_10"><10></label><label for="__tabbed_5_11"><11></label><label for="__tabbed_5_12"><12></label><label for="__tabbed_5_13"><13></label><label for="__tabbed_5_14"><14></label><label for="__tabbed_5_15"><15></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -4423,7 +4423,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p align="center"> 图:零钱兑换问题的动态规划过程 </p>
|
||||
<p align="center"> 图 14-25 零钱兑换问题的动态规划过程 </p>
|
||||
|
||||
<h3 id="3_1">3. 状态压缩<a class="headerlink" href="#3_1" title="Permanent link">¶</a></h3>
|
||||
<p>零钱兑换的状态压缩的处理方式和完全背包一致。</p>
|
||||
@@ -4676,7 +4676,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
|
||||
<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>。</p>
|
||||
</div>
|
||||
<p><img alt="零钱兑换问题 II 的示例数据" src="../unbounded_knapsack_problem.assets/coin_change_ii_example.png" /></p>
|
||||
<p align="center"> 图:零钱兑换问题 II 的示例数据 </p>
|
||||
<p align="center"> 图 14-26 零钱兑换问题 II 的示例数据 </p>
|
||||
|
||||
<h3 id="1_2">1. 动态规划思路<a class="headerlink" href="#1_2" title="Permanent link">¶</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>
|
||||
|
||||
Reference in New Issue
Block a user