mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-04 03:34:21 +00:00
deploy
This commit is contained in:
@@ -3450,7 +3450,7 @@
|
||||
|
||||
|
||||
<h1 id="142">14.2 动态规划问题特性<a class="headerlink" href="#142" title="Permanent link">¶</a></h1>
|
||||
<p>在上节中,我们学习了动态规划是如何通过子问题分解来求解问题的。实际上,子问题分解是一种通用的算法思路,在分治、动态规划、回溯中的侧重点不同:</p>
|
||||
<p>在上节中,我们学习了动态规划是如何通过子问题分解来求解问题的。实际上,子问题分解是一种通用的算法思路,在分治、动态规划、回溯中的侧重点不同。</p>
|
||||
<ul>
|
||||
<li>分治算法递归地将原问题划分为多个相互独立的子问题,直至最小子问题,并在回溯中合并子问题的解,最终得到原问题的解。</li>
|
||||
<li>动态规划也对问题进行递归分解,但与分治算法的主要区别是,动态规划中的子问题是相互依赖的,在分解过程中会出现许多重叠子问题。</li>
|
||||
@@ -3463,18 +3463,18 @@
|
||||
<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>如图 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>如图 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"> 图 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>
|
||||
<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">\[
|
||||
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
\]</div>
|
||||
<p>这便可以引出最优子结构的含义:<strong>原问题的最优解是从子问题的最优解构建得来的</strong>。</p>
|
||||
<p>本题显然具有最优子结构:我们从两个子问题最优解 <span class="arithmatex">\(dp[i-1]\)</span> , <span class="arithmatex">\(dp[i-2]\)</span> 中挑选出较优的那一个,并用它构建出原问题 <span class="arithmatex">\(dp[i]\)</span> 的最优解。</p>
|
||||
<p>本题显然具有最优子结构:我们从两个子问题最优解 <span class="arithmatex">\(dp[i-1]\)</span> 和 <span class="arithmatex">\(dp[i-2]\)</span> 中挑选出较优的那一个,并用它构建出原问题 <span class="arithmatex">\(dp[i]\)</span> 的最优解。</p>
|
||||
<p>那么,上节的爬楼梯题目有没有最优子结构呢?它的目标是求解方案数量,看似是一个计数问题,但如果换一种问法:“求解最大方案数量”。我们意外地发现,<strong>虽然题目修改前后是等价的,但最优子结构浮现出来了</strong>:第 <span class="arithmatex">\(n\)</span> 阶最大方案数量等于第 <span class="arithmatex">\(n-1\)</span> 阶和第 <span class="arithmatex">\(n-2\)</span> 阶最大方案数量之和。所以说,最优子结构的解释方式比较灵活,在不同问题中会有不同的含义。</p>
|
||||
<p>根据状态转移方程,以及初始状态 <span class="arithmatex">\(dp[1] = cost[1]\)</span> , <span class="arithmatex">\(dp[2] = cost[2]\)</span> ,我们就可以得到动态规划代码。</p>
|
||||
<p>根据状态转移方程,以及初始状态 <span class="arithmatex">\(dp[1] = cost[1]\)</span> 和 <span class="arithmatex">\(dp[2] = cost[2]\)</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">
|
||||
<div class="tabbed-block">
|
||||
@@ -3838,7 +3838,7 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
|
||||
<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>
|
||||
<p>为此,我们需要扩展状态定义:<strong>状态 <span class="arithmatex">\([i, j]\)</span> 表示处在第 <span class="arithmatex">\(i\)</span> 阶、并且上一轮跳了 <span class="arithmatex">\(j\)</span> 阶</strong>,其中 <span class="arithmatex">\(j \in \{1, 2\}\)</span> 。此状态定义有效地区分了上一轮跳了 <span class="arithmatex">\(1\)</span> 阶还是 <span class="arithmatex">\(2\)</span> 阶,我们可以据此来决定下一步该怎么跳:</p>
|
||||
<p>为此,我们需要扩展状态定义:<strong>状态 <span class="arithmatex">\([i, j]\)</span> 表示处在第 <span class="arithmatex">\(i\)</span> 阶、并且上一轮跳了 <span class="arithmatex">\(j\)</span> 阶</strong>,其中 <span class="arithmatex">\(j \in \{1, 2\}\)</span> 。此状态定义有效地区分了上一轮跳了 <span class="arithmatex">\(1\)</span> 阶还是 <span class="arithmatex">\(2\)</span> 阶,我们可以据此来决定下一步该怎么跳。</p>
|
||||
<ul>
|
||||
<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>
|
||||
@@ -4061,10 +4061,10 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>在上面的案例中,由于仅需多考虑前面一个状态,我们仍然可以通过扩展状态定义,使得问题恢复无后效性。然而,许多问题具有非常严重的“有后效性”,例如:</p>
|
||||
<p>在上面的案例中,由于仅需多考虑前面一个状态,我们仍然可以通过扩展状态定义,使得问题重新满足无后效性。然而,某些问题具有非常严重的“有后效性”。</p>
|
||||
<div class="admonition question">
|
||||
<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">\(i\)</span> 阶时,系统自动会给第 <span class="arithmatex">\(2i\)</span> 阶上放上障碍物,之后所有轮都不允许跳到第 <span class="arithmatex">\(2i\)</span> 阶上</strong>。例如,前两轮分别跳到了第 <span class="arithmatex">\(2, 3\)</span> 阶上,则之后就不能跳到第 <span class="arithmatex">\(4, 6\)</span> 阶上。请问有多少种方案可以爬到楼顶。</p>
|
||||
<p>给定一个共有 <span class="arithmatex">\(n\)</span> 阶的楼梯,你每步可以上 <span class="arithmatex">\(1\)</span> 阶或者 <span class="arithmatex">\(2\)</span> 阶。<strong>规定当爬到第 <span class="arithmatex">\(i\)</span> 阶时,系统自动会给第 <span class="arithmatex">\(2i\)</span> 阶上放上障碍物,之后所有轮都不允许跳到第 <span class="arithmatex">\(2i\)</span> 阶上</strong>。例如,前两轮分别跳到了第 <span class="arithmatex">\(2\)</span>、<span class="arithmatex">\(3\)</span> 阶上,则之后就不能跳到第 <span class="arithmatex">\(4\)</span>、<span class="arithmatex">\(6\)</span> 阶上。请问有多少种方案可以爬到楼顶。</p>
|
||||
</div>
|
||||
<p>在这个问题中,下次跳跃依赖于过去所有的状态,因为每一次跳跃都会在更高的阶梯上设置障碍,并影响未来的跳跃。对于这类问题,动态规划往往难以解决。</p>
|
||||
<p>实际上,许多复杂的组合优化问题(例如旅行商问题)都不满足无后效性。对于这类问题,我们通常会选择使用其他方法,例如启发式搜索、遗传算法、强化学习等,从而在有限时间内得到可用的局部最优解。</p>
|
||||
|
||||
@@ -3518,7 +3518,7 @@
|
||||
|
||||
|
||||
<h1 id="143">14.3 动态规划解题思路<a class="headerlink" href="#143" title="Permanent link">¶</a></h1>
|
||||
<p>上两节介绍了动态规划问题的主要特征,接下来我们一起探究两个更加实用的问题:</p>
|
||||
<p>上两节介绍了动态规划问题的主要特征,接下来我们一起探究两个更加实用的问题。</p>
|
||||
<ol>
|
||||
<li>如何判断一个问题是不是动态规划问题?</li>
|
||||
<li>求解动态规划问题该从何处入手,完整步骤是什么?</li>
|
||||
@@ -3527,12 +3527,12 @@
|
||||
<p>总的来说,如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常就适合用动态规划求解。然而,我们很难从问题描述上直接提取出这些特性。因此我们通常会放宽条件,<strong>先观察问题是否适合使用回溯(穷举)解决</strong>。</p>
|
||||
<p><strong>适合用回溯解决的问题通常满足“决策树模型”</strong>,这种问题可以使用树形结构来描述,其中每一个节点代表一个决策,每一条路径代表一个决策序列。</p>
|
||||
<p>换句话说,如果问题包含明确的决策概念,并且解是通过一系列决策产生的,那么它就满足决策树模型,通常可以使用回溯来解决。</p>
|
||||
<p>在此基础上,还有一些动态规划问题的“加分项”,包括:</p>
|
||||
<p>在此基础上,动态规划问题还有一些判断的“加分项”。</p>
|
||||
<ul>
|
||||
<li>问题包含最大(小)或最多(少)等最优化描述。</li>
|
||||
<li>问题的状态能够使用一个列表、多维矩阵或树来表示,并且一个状态与其周围的状态存在递推关系。</li>
|
||||
</ul>
|
||||
<p>而相应的“减分项”包括:</p>
|
||||
<p>相应地,也存在一些“减分项”。</p>
|
||||
<ul>
|
||||
<li>问题的目标是找出所有可能的解决方案,而不是找出最优解。</li>
|
||||
<li>问题描述中有明显的排列组合的特征,需要返回具体的多个方案。</li>
|
||||
@@ -3588,7 +3588,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
</div>
|
||||
<p>根据以上分析,我们已经可以直接写出动态规划代码。然而子问题分解是一种从顶至底的思想,因此按照“暴力搜索 <span class="arithmatex">\(\rightarrow\)</span> 记忆化搜索 <span class="arithmatex">\(\rightarrow\)</span> 动态规划”的顺序实现更加符合思维习惯。</p>
|
||||
<h3 id="1">1. 方法一:暴力搜索<a class="headerlink" href="#1" title="Permanent link">¶</a></h3>
|
||||
<p>从状态 <span class="arithmatex">\([i, j]\)</span> 开始搜索,不断分解为更小的状态 <span class="arithmatex">\([i-1, j]\)</span> 和 <span class="arithmatex">\([i, j-1]\)</span> ,包括以下递归要素:</p>
|
||||
<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>
|
||||
<li><strong>返回值</strong>:从 <span class="arithmatex">\([0, 0]\)</span> 到 <span class="arithmatex">\([i, j]\)</span> 的最小路径和 <span class="arithmatex">\(dp[i, j]\)</span> 。</li>
|
||||
|
||||
@@ -3483,16 +3483,16 @@
|
||||
<h3 id="1">1. 动态规划思路<a class="headerlink" href="#1" title="Permanent link">¶</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>
|
||||
<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>
|
||||
<ul>
|
||||
<li>若 <span class="arithmatex">\(s[n-1]\)</span> 和 <span class="arithmatex">\(t[m-1]\)</span> 相同,我们可以跳过它们,直接考虑 <span class="arithmatex">\(s[n-2]\)</span> 和 <span class="arithmatex">\(t[m-2]\)</span> 。</li>
|
||||
<li>若 <span class="arithmatex">\(s[n-1]\)</span> 和 <span class="arithmatex">\(t[m-1]\)</span> 不同,我们需要对 <span class="arithmatex">\(s\)</span> 进行一次编辑(插入、删除、替换),使得两字符串尾部的字符相同,从而可以跳过它们,考虑规模更小的问题。</li>
|
||||
</ul>
|
||||
<p>也就是说,我们在字符串 <span class="arithmatex">\(s\)</span> 中进行的每一轮决策(编辑操作),都会使得 <span class="arithmatex">\(s\)</span> 和 <span class="arithmatex">\(t\)</span> 中剩余的待匹配字符发生变化。因此,状态为当前在 <span class="arithmatex">\(s\)</span> , <span class="arithmatex">\(t\)</span> 中考虑的第 <span class="arithmatex">\(i\)</span> , <span class="arithmatex">\(j\)</span> 个字符,记为 <span class="arithmatex">\([i, j]\)</span> 。</p>
|
||||
<p>也就是说,我们在字符串 <span class="arithmatex">\(s\)</span> 中进行的每一轮决策(编辑操作),都会使得 <span class="arithmatex">\(s\)</span> 和 <span class="arithmatex">\(t\)</span> 中剩余的待匹配字符发生变化。因此,状态为当前在 <span class="arithmatex">\(s\)</span> 和 <span class="arithmatex">\(t\)</span> 中考虑的第 <span class="arithmatex">\(i\)</span> 和 <span class="arithmatex">\(j\)</span> 个字符,记为 <span class="arithmatex">\([i, j]\)</span> 。</p>
|
||||
<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> ,可根据不同编辑操作分为图 14-29 所示的三种情况:</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>
|
||||
@@ -3501,7 +3501,7 @@
|
||||
<p><img alt="编辑距离的状态转移" src="../edit_distance_problem.assets/edit_distance_state_transfer.png" /></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>
|
||||
<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">\[
|
||||
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
|
||||
\]</div>
|
||||
@@ -3834,7 +3834,7 @@ dp[i, j] = dp[i-1, j-1]
|
||||
<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>
|
||||
<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>
|
||||
<div class="tabbed-content">
|
||||
|
||||
@@ -3812,7 +3812,7 @@
|
||||
<p>回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。</p>
|
||||
<p>我们可以尝试从问题分解的角度分析这道题。设爬到第 <span class="arithmatex">\(i\)</span> 阶共有 <span class="arithmatex">\(dp[i]\)</span> 种方案,那么 <span class="arithmatex">\(dp[i]\)</span> 就是原问题,其子问题包括:</p>
|
||||
<div class="arithmatex">\[
|
||||
dp[i-1] , dp[i-2] , \dots , dp[2] , dp[1]
|
||||
dp[i-1], dp[i-2], \dots, dp[2], dp[1]
|
||||
\]</div>
|
||||
<p>由于每轮只能上 <span class="arithmatex">\(1\)</span> 阶或 <span class="arithmatex">\(2\)</span> 阶,因此当我们站在第 <span class="arithmatex">\(i\)</span> 阶楼梯上时,上一轮只可能站在第 <span class="arithmatex">\(i - 1\)</span> 阶或第 <span class="arithmatex">\(i - 2\)</span> 阶上。换句话说,我们只能从第 <span class="arithmatex">\(i -1\)</span> 阶或第 <span class="arithmatex">\(i - 2\)</span> 阶前往第 <span class="arithmatex">\(i\)</span> 阶。</p>
|
||||
<p>由此便可得出一个重要推论:<strong>爬到第 <span class="arithmatex">\(i - 1\)</span> 阶的方案数加上爬到第 <span class="arithmatex">\(i - 2\)</span> 阶的方案数就等于爬到第 <span class="arithmatex">\(i\)</span> 阶的方案数</strong>。公式如下:</p>
|
||||
@@ -3823,11 +3823,7 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
<p><img alt="方案数量递推关系" src="../intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png" /></p>
|
||||
<p align="center"> 图 14-2 方案数量递推关系 </p>
|
||||
|
||||
<p>我们可以根据递推公式得到暴力搜索解法:</p>
|
||||
<ul>
|
||||
<li>以 <span class="arithmatex">\(dp[n]\)</span> 为起始点,<strong>递归地将一个较大问题拆解为两个较小问题的和</strong>,直至到达最小子问题 <span class="arithmatex">\(dp[1]\)</span> 和 <span class="arithmatex">\(dp[2]\)</span> 时返回。</li>
|
||||
<li>最小子问题的解 <span class="arithmatex">\(dp[1] = 1\)</span> , <span class="arithmatex">\(dp[2] = 2\)</span> 是已知的,代表爬到第 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span> 阶分别有 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span> 种方案。</li>
|
||||
</ul>
|
||||
<p>我们可以根据递推公式得到暴力搜索解法。以 <span class="arithmatex">\(dp[n]\)</span> 为起始点,<strong>递归地将一个较大问题拆解为两个较小问题的和</strong>,直至到达最小子问题 <span class="arithmatex">\(dp[1]\)</span> 和 <span class="arithmatex">\(dp[2]\)</span> 时返回。其中,最小子问题的解是已知的,即 <span class="arithmatex">\(dp[1] = 1\)</span>、<span class="arithmatex">\(dp[2] = 2\)</span> ,表示爬到第 <span class="arithmatex">\(1\)</span>、<span class="arithmatex">\(2\)</span> 阶分别有 <span class="arithmatex">\(1\)</span>、<span class="arithmatex">\(2\)</span> 种方案。</p>
|
||||
<p>观察以下代码,它和标准回溯代码都属于深度优先搜索,但更加简洁。</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">
|
||||
@@ -4030,10 +4026,10 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
<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>
|
||||
<p>为了提升算法效率,<strong>我们希望所有的重叠子问题都只被计算一次</strong>。为此,我们声明一个数组 <code>mem</code> 来记录每个子问题的解,并在搜索过程中将重叠子问题剪枝。</p>
|
||||
<ol>
|
||||
<li>当首次计算 <span class="arithmatex">\(dp[i]\)</span> 时,我们将其记录至 <code>mem[i]</code> ,以便之后使用。</li>
|
||||
<li>当再次需要计算 <span class="arithmatex">\(dp[i]\)</span> 时,我们便可直接从 <code>mem[i]</code> 中获取结果,从而将重叠子问题剪枝。</li>
|
||||
<li>当再次需要计算 <span class="arithmatex">\(dp[i]\)</span> 时,我们便可直接从 <code>mem[i]</code> 中获取结果,从而避免重复计算该子问题。</li>
|
||||
</ol>
|
||||
<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">
|
||||
@@ -4527,10 +4523,10 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
<p align="center"> 图 14-5 爬楼梯的动态规划过程 </p>
|
||||
|
||||
<p>与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的某个特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 <span class="arithmatex">\(i\)</span> 。</p>
|
||||
<p>总结以上,动态规划的常用术语包括:</p>
|
||||
<p>根据以上内容,我们可以总结出动态规划的常用术语。</p>
|
||||
<ul>
|
||||
<li>将数组 <code>dp</code> 称为「<span class="arithmatex">\(dp\)</span> 表」,<span class="arithmatex">\(dp[i]\)</span> 表示状态 <span class="arithmatex">\(i\)</span> 对应子问题的解。</li>
|
||||
<li>将最小子问题对应的状态(即第 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span> 阶楼梯)称为「初始状态」。</li>
|
||||
<li>将最小子问题对应的状态(即第 <span class="arithmatex">\(1\)</span> 和 <span class="arithmatex">\(2\)</span> 阶楼梯)称为「初始状态」。</li>
|
||||
<li>将递推公式 <span class="arithmatex">\(dp[i] = dp[i-1] + dp[i-2]\)</span> 称为「状态转移方程」。</li>
|
||||
</ul>
|
||||
<h2 id="1414">14.1.4 空间优化<a class="headerlink" href="#1414" title="Permanent link">¶</a></h2>
|
||||
|
||||
@@ -3482,7 +3482,7 @@
|
||||
<p>在本节中,我们先来求解最常见的 0-1 背包问题。</p>
|
||||
<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> 的背包。每个物品只能选择一次,问在不超过背包容量下能放入物品的最大价值。</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>观察图 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>
|
||||
@@ -3510,7 +3510,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
<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">1. 方法一:暴力搜索<a class="headerlink" href="#1" title="Permanent link">¶</a></h3>
|
||||
<p>搜索代码包含以下要素:</p>
|
||||
<p>搜索代码包含以下要素。</p>
|
||||
<ul>
|
||||
<li><strong>递归参数</strong>:状态 <span class="arithmatex">\([i, c]\)</span> 。</li>
|
||||
<li><strong>返回值</strong>:子问题的解 <span class="arithmatex">\(dp[i, c]\)</span> 。</li>
|
||||
|
||||
@@ -3630,7 +3630,7 @@
|
||||
<h2 id="1451">14.5.1 完全背包<a class="headerlink" href="#1451" title="Permanent link">¶</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"> 图 14-22 完全背包问题的示例数据 </p>
|
||||
@@ -3641,7 +3641,7 @@
|
||||
<li>在 0-1 背包中,每个物品只有一个,因此将物品 <span class="arithmatex">\(i\)</span> 放入背包后,只能从前 <span class="arithmatex">\(i-1\)</span> 个物品中选择。</li>
|
||||
<li>在完全背包中,每个物品有无数个,因此将物品 <span class="arithmatex">\(i\)</span> 放入背包后,<strong>仍可以从前 <span class="arithmatex">\(i\)</span> 个物品中选择</strong>。</li>
|
||||
</ul>
|
||||
<p>这就导致了状态转移的变化,对于状态 <span class="arithmatex">\([i, c]\)</span> 有:</p>
|
||||
<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> :与 0-1 背包不同,转移至 <span class="arithmatex">\([i, c-wgt[i-1]]\)</span> 。</li>
|
||||
@@ -4114,7 +4114,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
<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>
|
||||
<p><strong>零钱兑换可以看作是完全背包的一种特殊情况</strong>,两者具有以下联系与不同点。</p>
|
||||
<ul>
|
||||
<li>两道题可以相互转换,“物品”对应于“硬币”、“物品重量”对应于“硬币面值”、“背包容量”对应于“目标金额”。</li>
|
||||
<li>优化目标相反,背包问题是要最大化物品价值,零钱兑换问题是要最小化硬币数量。</li>
|
||||
@@ -4124,7 +4124,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
<p>状态 <span class="arithmatex">\([i, a]\)</span> 对应的子问题为:<strong>前 <span class="arithmatex">\(i\)</span> 种硬币能够凑出金额 <span class="arithmatex">\(a\)</span> 的最少硬币个数</strong>,记为 <span class="arithmatex">\(dp[i, a]\)</span> 。</p>
|
||||
<p>二维 <span class="arithmatex">\(dp\)</span> 表的尺寸为 <span class="arithmatex">\((n+1) \times (amt+1)\)</span> 。</p>
|
||||
<p><strong>第二步:找出最优子结构,进而推导出状态转移方程</strong></p>
|
||||
<p>与完全背包的状态转移方程基本相同,不同点在于:</p>
|
||||
<p>本题与完全背包的状态转移方程存在以下两个差异。</p>
|
||||
<ul>
|
||||
<li>本题要求最小值,因此需将运算符 <span class="arithmatex">\(\max()\)</span> 更改为 <span class="arithmatex">\(\min()\)</span> 。</li>
|
||||
<li>优化主体是硬币数量而非商品价值,因此在选中硬币时执行 <span class="arithmatex">\(+1\)</span> 即可。</li>
|
||||
|
||||
Reference in New Issue
Block a user