This commit is contained in:
krahets
2023-08-20 23:28:04 +08:00
parent 26a2e7f171
commit 47b7d6fd44
49 changed files with 161 additions and 162 deletions

View File

@@ -3422,9 +3422,9 @@
<h1 id="142">14.2 &nbsp; 动态规划问题特性<a class="headerlink" href="#142" title="Permanent link">&para;</a></h1>
<p>在上节中,我们学习了动态规划是如何通过子问题分解来求解问题的。实际上,子问题分解是一种通用的算法思路,在分治、动态规划、回溯中的侧重点不同:</p>
<ul>
<li>分治算法递归地将原问题划分为多个相互独立的子问题,直至最小子问题,并在回溯中合并子问题的解,最终得到原问题的解。</li>
<li>动态规划也对问题进行递归分解,但与分治算法的主要区别是,动态规划中的子问题是相互依赖的,在分解过程中会出现许多重叠子问题。</li>
<li>回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作为一个子问题。</li>
<li>分治算法递归地将原问题划分为多个相互独立的子问题,直至最小子问题,并在回溯中合并子问题的解,最终得到原问题的解。</li>
<li>动态规划也对问题进行递归分解,但与分治算法的主要区别是,动态规划中的子问题是相互依赖的,在分解过程中会出现许多重叠子问题。</li>
<li>回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作为一个子问题。</li>
</ul>
<p>实际上,动态规划常用来求解最优化问题,它们不仅包含重叠子问题,还具有另外两大特性:最优子结构、无后效性。</p>
<h2 id="1421">14.2.1 &nbsp; 最优子结构<a class="headerlink" href="#1421" title="Permanent link">&para;</a></h2>
@@ -3441,7 +3441,7 @@
<div class="arithmatex">\[
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
\]</div>
<p>这便可以引出最优子结构的含义:<strong>原问题的最优解是从子问题的最优解构建得来的</strong></p>
<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>那么,上节的爬楼梯题目有没有最优子结构呢?它的目标是求解方案数量,看似是一个计数问题,但如果换一种问法:“求解最大方案数量”。我们意外地发现,<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>
@@ -3794,7 +3794,7 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
</div>
</div>
<h2 id="1422">14.2.2 &nbsp; 无后效性<a class="headerlink" href="#1422" title="Permanent link">&para;</a></h2>
<p>无后效性是动态规划能够有效解决问题的重要特性之一,定义为:<strong>给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关</strong></p>
<p>无后效性是动态规划能够有效解决问题的重要特性之一,定义为:<strong>给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关</strong></p>
<p>以爬楼梯问题为例,给定状态 <span class="arithmatex">\(i\)</span> ,它会发展出状态 <span class="arithmatex">\(i+1\)</span> 和状态 <span class="arithmatex">\(i+2\)</span> ,分别对应跳 <span class="arithmatex">\(1\)</span> 步和跳 <span class="arithmatex">\(2\)</span> 步。在做出这两种选择时,我们无须考虑状态 <span class="arithmatex">\(i\)</span> 之前的状态,它们对状态 <span class="arithmatex">\(i\)</span> 的未来没有影响。</p>
<p>然而,如果我们向爬楼梯问题添加一个约束,情况就不一样了。</p>
<div class="admonition question">

View File

@@ -3510,7 +3510,7 @@
<p>如果一个问题满足决策树模型,并具有较为明显的“加分项“,我们就可以假设它是一个动态规划问题,并在求解过程中验证它。</p>
<h2 id="1432">14.3.2 &nbsp; 问题求解步骤<a class="headerlink" href="#1432" title="Permanent link">&para;</a></h2>
<p>动态规划的解题流程会因问题的性质和难度而有所不同,但通常遵循以下步骤:描述决策,定义状态,建立 <span class="arithmatex">\(dp\)</span> 表,推导状态转移方程,确定边界条件等。</p>
<p>为了更形象地展示解题步骤,我们使用一个经典问题最小路径和来举例。</p>
<p>为了更形象地展示解题步骤,我们使用一个经典问题最小路径和来举例。</p>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>给定一个 <span class="arithmatex">\(n \times m\)</span> 的二维网格 <code>grid</code> ,网格中的每个单元格包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回从左上角到右下角的最小路径和。</p>

View File

@@ -3448,7 +3448,7 @@
<h1 id="141">14.1 &nbsp; 初探动态规划<a class="headerlink" href="#141" title="Permanent link">&para;</a></h1>
<p>「动态规划 Dynamic Programming」是一个重要的算法范式它将一个问题分解为一系列更小的子问题并通过存储子问题的解来避免重复计算从而大幅提升时间效率。</p>
<p>「动态规划 dynamic programming」是一个重要的算法范式它将一个问题分解为一系列更小的子问题并通过存储子问题的解来避免重复计算从而大幅提升时间效率。</p>
<p>在本节中,我们从一个经典例题入手,先给出它的暴力回溯解法,观察其中包含的重叠子问题,再逐步导出更高效的动态规划解法。</p>
<div class="admonition question">
<p class="admonition-title">爬楼梯</p>
@@ -3997,7 +3997,7 @@ dp[i] = dp[i-1] + dp[i-2]
<p><img alt="爬楼梯对应递归树" src="../intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png" /></p>
<p align="center"> 图:爬楼梯对应递归树 </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>观察上图发现,<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 &nbsp; 方法二:记忆化搜索<a class="headerlink" href="#1412" title="Permanent link">&para;</a></h2>
<p>为了提升算法效率,<strong>我们希望所有的重叠子问题都只被计算一次</strong>。为此,我们声明一个数组 <code>mem</code> 来记录每个子问题的解,并在搜索过程中这样做:</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB