mirror of
https://github.com/krahets/hello-algo.git
synced 2025-07-25 03:08:54 +08:00
deploy
This commit is contained in:
@ -2198,13 +2198,8 @@
|
||||
|
||||
|
||||
<h1 id="131">13.1. 初探动态规划<a class="headerlink" href="#131" title="Permanent link">¶</a></h1>
|
||||
<p>动态规划(Dynamic Programming)是一种用于解决复杂问题的优化算法,它把一个问题分解为一系列更小的子问题,并把子问题的解存储起来以供后续使用,从而避免了重复计算,提升了解题效率。</p>
|
||||
<p>在本节中,我们先从一个动态规划经典例题入手,学习动态规划是如何高效地求解问题的,包括:</p>
|
||||
<ol>
|
||||
<li>如何暴力求解动态规划问题,什么是重叠子问题。</li>
|
||||
<li>如何向暴力搜索引入记忆化处理,从而优化时间复杂度。</li>
|
||||
<li>从递归解法引出动态规划解法,以及如何优化空间复杂度。</li>
|
||||
</ol>
|
||||
<p>「动态规划 Dynamic Programming」是一种用于解决复杂问题的优化算法,它把一个问题分解为一系列更小的子问题,并把子问题的解存储起来以供后续使用,从而避免了重复计算,提升了解题效率。</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> 阶,请问有多少种方案可以爬到楼顶。</p>
|
||||
@ -2213,8 +2208,7 @@
|
||||
<p><img alt="爬到第 3 阶的方案数量" src="../intro_to_dynamic_programming.assets/climbing_stairs_example.png" /></p>
|
||||
<p align="center"> Fig. 爬到第 3 阶的方案数量 </p>
|
||||
|
||||
<p><strong>不考虑效率的前提下,动态规划问题理论上都可以使用回溯算法解决</strong>,因为回溯算法本质上就是穷举,它能够遍历决策树的所有可能的状态,并从中记录需要的解。</p>
|
||||
<p>对于本题,我们可以将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上 <span class="arithmatex">\(1\)</span> 阶或 <span class="arithmatex">\(2\)</span> 阶,每当到达楼梯顶部时就将方案数量加 <span class="arithmatex">\(1\)</span> 。</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:11"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JavaScript</label><label for="__tabbed_1_6">TypeScript</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@ -2347,8 +2341,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2 id="1311">13.1.1. 方法一:暴力搜索<a class="headerlink" href="#1311" title="Permanent link">¶</a></h2>
|
||||
<p>然而,爬楼梯并不是典型的回溯问题,更适合从分治的角度进行解析。在分治算法中,原问题被分解为较小的子问题,通过组合子问题的解得到原问题的解。例如,归并排序将一个长数组从顶至底地划分为两个短数组,再从底至顶地将已排序的短数组进行排序。</p>
|
||||
<p>对于本题,设爬到第 <span class="arithmatex">\(i\)</span> 阶共有 <span class="arithmatex">\(dp[i]\)</span> 种方案,那么 <span class="arithmatex">\(dp[i]\)</span> 就是原问题,其子问题包括:</p>
|
||||
<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] , \cdots , dp[2] , dp[1]
|
||||
\]</div>
|
||||
@ -2356,11 +2350,12 @@ dp[i-1] , dp[i-2] , \cdots , dp[2] , dp[1]
|
||||
<div class="arithmatex">\[
|
||||
dp[i] = dp[i-1] + dp[i-2]
|
||||
\]</div>
|
||||
<p><img alt="方案数量递推公式" src="../intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png" /></p>
|
||||
<p align="center"> Fig. 方案数量递推公式 </p>
|
||||
<p><img alt="方案数量递推关系" src="../intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png" /></p>
|
||||
<p align="center"> Fig. 方案数量递推关系 </p>
|
||||
|
||||
<p>基于此递推公式,我们可以写出递归代码:以 <span class="arithmatex">\(dp[n]\)</span> 为起始点,<strong>从顶至底地将一个较大问题拆解为两个较小问题</strong>,直至到达最小子问题 <span class="arithmatex">\(dp[1]\)</span> 和 <span class="arithmatex">\(dp[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>
|
||||
<p>也就是说,在爬楼梯问题中,<strong>各个子问题之间不是相互独立的,原问题的解可以由子问题的解构成</strong>。</p>
|
||||
<p>我们可以基于此递推公式写出暴力搜索代码:以 <span class="arithmatex">\(dp[n]\)</span> 为起始点,<strong>从顶至底地将一个较大问题拆解为两个较小问题的和</strong>,直至到达最小子问题 <span class="arithmatex">\(dp[1]\)</span> 和 <span class="arithmatex">\(dp[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:11"><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" /><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">JavaScript</label><label for="__tabbed_2_6">TypeScript</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></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@ -2603,7 +2598,7 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
<h2 id="1313">13.1.3. 方法三:动态规划<a class="headerlink" href="#1313" title="Permanent link">¶</a></h2>
|
||||
<p><strong>记忆化搜索是一种“从顶至底”的方法</strong>:我们从原问题(根节点)开始,递归地将较大子问题分解为较小子问题,直至解已知的最小子问题(叶节点);最终通过回溯将子问题的解逐层收集,得到原问题的解。</p>
|
||||
<p><strong>我们也可以直接“从底至顶”进行求解</strong>,得到标准的动态规划解法:从最小子问题开始,迭代地求解较大子问题,直至得到原问题的解。</p>
|
||||
<p>由于没有回溯过程,动态规划可以直接基于循环实现。我们初始化一个数组 <code>dp</code> 来存储子问题的解,从最小子问题开始,逐步求解较大子问题。在以下代码中,数组 <code>dp</code> 起到了记忆化搜索中数组 <code>mem</code> 相同的记录作用。</p>
|
||||
<p>由于动态规划不包含回溯过程,因此无需使用递归,而可以直接基于递推实现。我们初始化一个数组 <code>dp</code> 来存储子问题的解,从最小子问题开始,逐步求解较大子问题。在以下代码中,数组 <code>dp</code> 起到了记忆化搜索中数组 <code>mem</code> 相同的记录作用。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="4:11"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Java</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Python</label><label for="__tabbed_4_4">Go</label><label for="__tabbed_4_5">JavaScript</label><label for="__tabbed_4_6">TypeScript</label><label for="__tabbed_4_7">C</label><label for="__tabbed_4_8">C#</label><label for="__tabbed_4_9">Swift</label><label for="__tabbed_4_10">Zig</label><label for="__tabbed_4_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
|
Reference in New Issue
Block a user