This commit is contained in:
krahets
2023-08-17 05:12:16 +08:00
parent 2014338a92
commit 5884de5246
70 changed files with 1890 additions and 1219 deletions

View File

@ -3456,7 +3456,7 @@
</div>
<p>如下图所示,对于一个 <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"> Fig. 爬到第 3 阶的方案数量 </p>
<p align="center"> 图:爬到第 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>
@ -3579,7 +3579,7 @@
<a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="w"> </span><span class="c1">// 当爬到第 n 阶时,方案数量加 1</span>
<a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="nx">res</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">res</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">);</span>
<a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span><span class="c1">// 遍历所有选择</span>
<a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="nx">choice</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">choices</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">choice</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">choices</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="w"> </span><span class="c1">// 剪枝:不允许越过第 n 阶</span>
<a id="__codelineno-4-8" name="__codelineno-4-8" href="#__codelineno-4-8"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">choice</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="k">break</span><span class="p">;</span>
<a id="__codelineno-4-9" name="__codelineno-4-9" href="#__codelineno-4-9"></a><span class="w"> </span><span class="c1">// 尝试:做出选择,更新状态</span>
@ -3610,7 +3610,7 @@
<a id="__codelineno-5-8" name="__codelineno-5-8" href="#__codelineno-5-8"></a><span class="w"> </span><span class="c1">// 当爬到第 n 阶时,方案数量加 1</span>
<a id="__codelineno-5-9" name="__codelineno-5-9" href="#__codelineno-5-9"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="nx">res</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">res</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">);</span>
<a id="__codelineno-5-10" name="__codelineno-5-10" href="#__codelineno-5-10"></a><span class="w"> </span><span class="c1">// 遍历所有选择</span>
<a id="__codelineno-5-11" name="__codelineno-5-11" href="#__codelineno-5-11"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">let</span><span class="w"> </span><span class="nx">choice</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">choices</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<a id="__codelineno-5-11" name="__codelineno-5-11" href="#__codelineno-5-11"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">choice</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">choices</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<a id="__codelineno-5-12" name="__codelineno-5-12" href="#__codelineno-5-12"></a><span class="w"> </span><span class="c1">// 剪枝:不允许越过第 n 阶</span>
<a id="__codelineno-5-13" name="__codelineno-5-13" href="#__codelineno-5-13"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">choice</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="nx">n</span><span class="p">)</span><span class="w"> </span><span class="k">break</span><span class="p">;</span>
<a id="__codelineno-5-14" name="__codelineno-5-14" href="#__codelineno-5-14"></a><span class="w"> </span><span class="c1">// 尝试:做出选择,更新状态</span>
@ -3791,7 +3791,7 @@ dp[i] = dp[i-1] + dp[i-2]
\]</div>
<p>这意味着在爬楼梯问题中,各个子问题之间存在递推关系,<strong>原问题的解可以由子问题的解构建得来</strong></p>
<p><img alt="方案数量递推关系" src="../intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png" /></p>
<p align="center"> Fig. 方案数量递推关系 </p>
<p align="center"> 图:方案数量递推关系 </p>
<p>我们可以根据递推公式得到暴力搜索解法:</p>
<ul>
@ -3995,7 +3995,7 @@ dp[i] = dp[i-1] + dp[i-2]
</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><img alt="爬楼梯对应递归树" src="../intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png" /></p>
<p align="center"> Fig. 爬楼梯对应递归树 </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>以此类推,子问题中包含更小的重叠子问题,子子孙孙无穷尽也。绝大部分计算资源都浪费在这些重叠的问题上。</p>
@ -4282,7 +4282,7 @@ dp[i] = dp[i-1] + dp[i-2]
</div>
<p>观察下图,<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"> Fig. 记忆化搜索对应递归树 </p>
<p align="center"> 图:记忆化搜索对应递归树 </p>
<h2 id="1413">14.1.3. &nbsp; 方法三:动态规划<a class="headerlink" href="#1413" title="Permanent link">&para;</a></h2>
<p><strong>记忆化搜索是一种“从顶至底”的方法</strong>:我们从原问题(根节点)开始,递归地将较大子问题分解为较小子问题,直至解已知的最小子问题(叶节点)。之后,通过回溯将子问题的解逐层收集,构建出原问题的解。</p>
@ -4500,7 +4500,7 @@ dp[i] = dp[i-1] + dp[i-2]
<li>将递推公式 <span class="arithmatex">\(dp[i] = dp[i-1] + dp[i-2]\)</span> 称为「状态转移方程」。</li>
</ul>
<p><img alt="爬楼梯的动态规划过程" src="../intro_to_dynamic_programming.assets/climbing_stairs_dp.png" /></p>
<p align="center"> Fig. 爬楼梯的动态规划过程 </p>
<p align="center"> 图:爬楼梯的动态规划过程 </p>
<h2 id="1414">14.1.4. &nbsp; 状态压缩<a class="headerlink" href="#1414" title="Permanent link">&para;</a></h2>
<p>细心的你可能发现,<strong>由于 <span class="arithmatex">\(dp[i]\)</span> 只与 <span class="arithmatex">\(dp[i-1]\)</span><span class="arithmatex">\(dp[i-2]\)</span> 有关,因此我们无需使用一个数组 <code>dp</code> 来存储所有子问题的解</strong>,而只需两个变量滚动前进即可。</p>