mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-04 19:54:20 +00:00
deploy
This commit is contained in:
@@ -2960,6 +2960,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3095,6 +3097,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -3323,7 +3353,13 @@
|
||||
|
||||
<h1 id="142">14.2. 动态规划问题特性<a class="headerlink" href="#142" title="Permanent link">¶</a></h1>
|
||||
<p>在上节中,我们学习了动态规划问题的暴力解法,从递归树中观察到海量的重叠子问题,以及了解到动态规划是如何通过记录解来优化时间复杂度的。</p>
|
||||
<p>实际上,动态规划最常用来求解最优方案问题,例如寻找最短路径、最大利润、最少时间等。<strong>这类问题不仅包含重叠子问题,往往还具有另外两大特性:最优子结构、无后效性</strong>。</p>
|
||||
<p>总的看来,<strong>子问题分解是一种通用的算法思路,在分治、动态规划、回溯中各有特点</strong>:</p>
|
||||
<ul>
|
||||
<li>分治算法将原问题划分为几个独立的子问题,然后递归解决子问题,最后合并子问题的解得到原问题的解。</li>
|
||||
<li>动态规划也是将原问题分解为多个子问题,但与分治算法的主要区别是,<strong>动态规划中的子问题往往不是相互独立的</strong>,原问题的解依赖于子问题的解,而子问题的解又依赖于更小的子问题的解。</li>
|
||||
<li>回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作为一个子问题。</li>
|
||||
</ul>
|
||||
<p>实际上,动态规划最常用来求解最优化问题。<strong>这类问题不仅包含重叠子问题,还具有另外两大特性:最优子结构、无后效性</strong>。</p>
|
||||
<h2 id="1421">14.2.1. 最优子结构<a class="headerlink" href="#1421" title="Permanent link">¶</a></h2>
|
||||
<p>我们对爬楼梯问题稍作改动,使之更加适合展示最优子结构概念。</p>
|
||||
<div class="admonition question">
|
||||
|
||||
@@ -2812,30 +2812,43 @@
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1432" class="md-nav__link">
|
||||
14.3.2. 问题求解
|
||||
14.3.2. 问题求解步骤
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.3.2. 问题求解步骤">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
方法一:暴力搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1433" class="md-nav__link">
|
||||
14.3.3. 方法一:暴力搜索
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_2" class="md-nav__link">
|
||||
方法二:记忆化搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1434" class="md-nav__link">
|
||||
14.3.4. 方法二:记忆化搜索
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_3" class="md-nav__link">
|
||||
方法三:动态规划
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1435" class="md-nav__link">
|
||||
14.3.5. 方法三:动态规划
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_4" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
@@ -2981,6 +2994,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3116,6 +3131,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -3314,30 +3357,43 @@
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1432" class="md-nav__link">
|
||||
14.3.2. 问题求解
|
||||
14.3.2. 问题求解步骤
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.3.2. 问题求解步骤">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
方法一:暴力搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1433" class="md-nav__link">
|
||||
14.3.3. 方法一:暴力搜索
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_2" class="md-nav__link">
|
||||
方法二:记忆化搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1434" class="md-nav__link">
|
||||
14.3.4. 方法二:记忆化搜索
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_3" class="md-nav__link">
|
||||
方法三:动态规划
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1435" class="md-nav__link">
|
||||
14.3.5. 方法三:动态规划
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_4" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
@@ -3384,7 +3440,7 @@
|
||||
<li>问题描述中有明显的排列组合的特征,需要返回具体的多个方案。</li>
|
||||
</ul>
|
||||
<p>如果一个问题满足决策树模型,并具有较为明显的“加分项“,我们就可以假设它是一个动态规划问题,并尝试求解它。</p>
|
||||
<h2 id="1432">14.3.2. 问题求解<a class="headerlink" href="#1432" title="Permanent link">¶</a></h2>
|
||||
<h2 id="1432">14.3.2. 问题求解步骤<a class="headerlink" href="#1432" title="Permanent link">¶</a></h2>
|
||||
<p>动态规划的解题流程可能会因问题的性质和难度而有所不同,但通常遵循以下步骤:描述决策,定义状态,建立 <span class="arithmatex">\(dp\)</span> 表,推导状态转移方程,确定边界条件等。</p>
|
||||
<p>为了更形象地展示解题步骤,我们使用一个经典问题「最小路径和」来举例。</p>
|
||||
<div class="admonition question">
|
||||
@@ -3432,7 +3488,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
<p>边界条件即初始状态,在搜索中用于剪枝,在动态规划中用于初始化 <span class="arithmatex">\(dp\)</span> 表。状态转移顺序的核心是要保证在计算当前问题时,所有它依赖的更小子问题都已经被正确地计算出来。</p>
|
||||
</div>
|
||||
<p>接下来,我们就可以实现动态规划代码了。然而,由于子问题分解是一种从顶至底的思想,因此按照“暴力搜索 <span class="arithmatex">\(\rightarrow\)</span> 记忆化搜索 <span class="arithmatex">\(\rightarrow\)</span> 动态规划”的顺序实现更加符合思维习惯。</p>
|
||||
<h2 id="1433">14.3.3. 方法一:暴力搜索<a class="headerlink" href="#1433" title="Permanent link">¶</a></h2>
|
||||
<h3 id="_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>
|
||||
<ul>
|
||||
<li><strong>递归参数</strong>:状态 <span class="arithmatex">\([i, j]\)</span> ;<strong>返回值</strong>:从 <span class="arithmatex">\([0, 0]\)</span> 到 <span class="arithmatex">\([i, j]\)</span> 的最小路径和 <span class="arithmatex">\(dp[i, j]\)</span> ;</li>
|
||||
@@ -3580,7 +3636,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
<p align="center"> Fig. 暴力搜索递归树 </p>
|
||||
|
||||
<p>每个状态都有向下和向右两种选择,从左上角走到右下角总共需要 <span class="arithmatex">\(m + n - 2\)</span> 步,所以最差时间复杂度为 <span class="arithmatex">\(O(2^{m + n})\)</span> 。请注意,这种计算方式未考虑临近网格边界的情况,当到达网络边界时只剩下一种选择。因此实际的路径数量会少一些。</p>
|
||||
<h2 id="1434">14.3.4. 方法二:记忆化搜索<a class="headerlink" href="#1434" title="Permanent link">¶</a></h2>
|
||||
<h3 id="_2">方法二:记忆化搜索<a class="headerlink" href="#_2" title="Permanent link">¶</a></h3>
|
||||
<p>为了避免重复计算重叠子问题,我们引入一个和网格 <code>grid</code> 相同尺寸的记忆列表 <code>mem</code> ,用于记录各个子问题的解,提升搜索效率。</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">
|
||||
@@ -3753,7 +3809,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
<p><img alt="记忆化搜索递归树" src="../dp_solution_pipeline.assets/min_path_sum_dfs_mem.png" /></p>
|
||||
<p align="center"> Fig. 记忆化搜索递归树 </p>
|
||||
|
||||
<h2 id="1435">14.3.5. 方法三:动态规划<a class="headerlink" href="#1435" title="Permanent link">¶</a></h2>
|
||||
<h3 id="_3">方法三:动态规划<a class="headerlink" href="#_3" title="Permanent link">¶</a></h3>
|
||||
<p>动态规划代码是从底至顶的,仅需循环即可实现。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="3:11"><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" /><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">JavaScript</label><label for="__tabbed_3_6">TypeScript</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></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -3967,6 +4023,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="_4">状态压缩<a class="headerlink" href="#_4" title="Permanent link">¶</a></h3>
|
||||
<p>如果希望进一步节省空间使用,可以考虑进行状态压缩。每个格子只与左边和上边的格子有关,因此我们可以只用一个单行数组来实现 <span class="arithmatex">\(dp\)</span> 表。</p>
|
||||
<p>由于数组 <code>dp</code> 只能表示一行的状态,因此我们无法提前初始化首列状态,而是在遍历每行中更新它。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="5:11"><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" /><div class="tabbed-labels"><label for="__tabbed_5_1">Java</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Python</label><label for="__tabbed_5_4">Go</label><label for="__tabbed_5_5">JavaScript</label><label for="__tabbed_5_6">TypeScript</label><label for="__tabbed_5_7">C</label><label for="__tabbed_5_8">C#</label><label for="__tabbed_5_9">Swift</label><label for="__tabbed_5_10">Zig</label><label for="__tabbed_5_11">Dart</label></div>
|
||||
|
||||
@@ -2835,6 +2835,25 @@
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__link md-nav__link--active" for="__toc">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
14.6. 编辑距离问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
|
||||
<a href="./" class="md-nav__link md-nav__link--active">
|
||||
|
||||
|
||||
@@ -2853,6 +2872,39 @@
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="目录">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
目录
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_2" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
@@ -2908,6 +2960,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3043,6 +3097,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -3226,6 +3308,28 @@
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
目录
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_2" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3294,6 +3398,7 @@ dp[i, j] = dp[i-1, j-1]
|
||||
<p><strong>第三步:确定边界条件和状态转移顺序</strong></p>
|
||||
<p>当两字符串都为空时,编辑步数为 <span class="arithmatex">\(0\)</span> ,即 <span class="arithmatex">\(dp[0, 0] = 0\)</span> 。当 <span class="arithmatex">\(s\)</span> 为空但 <span class="arithmatex">\(t\)</span> 不为空时,最少编辑步数等于 <span class="arithmatex">\(t\)</span> 的长度,即 <span class="arithmatex">\(dp[0, j] = j\)</span> 。当 <span class="arithmatex">\(s\)</span> 不为空但 <span class="arithmatex">\(t\)</span> 为空时,等于 <span class="arithmatex">\(s\)</span> 的长度,即 <span class="arithmatex">\(dp[i, 0] = i\)</span> 。</p>
|
||||
<p>观察状态转移方程,解 <span class="arithmatex">\(dp[i, j]\)</span> 依赖左方、上方、左上方的解,因此通过两层循环正序遍历整个 <span class="arithmatex">\(dp\)</span> 表即可。</p>
|
||||
<h3 id="_1">代码实现<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
||||
<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">
|
||||
@@ -3532,6 +3637,7 @@ dp[i, j] = dp[i-1, j-1]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="_2">状态压缩<a class="headerlink" href="#_2" title="Permanent link">¶</a></h3>
|
||||
<p>下面考虑状态压缩,将 <span class="arithmatex">\(dp\)</span> 表的第一维删除。由于 <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:11"><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" /><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">JavaScript</label><label for="__tabbed_3_6">TypeScript</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></div>
|
||||
|
||||
@@ -2898,6 +2898,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3033,6 +3035,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -2766,6 +2766,13 @@
|
||||
14.1.3. 方法三:动态规划
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1414" class="md-nav__link">
|
||||
14.1.4. 状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
@@ -2967,6 +2974,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3102,6 +3111,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -3310,6 +3347,13 @@
|
||||
14.1.3. 方法三:动态规划
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1414" class="md-nav__link">
|
||||
14.1.4. 状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
@@ -4041,6 +4085,7 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
<p><img alt="爬楼梯的动态规划过程" src="../intro_to_dynamic_programming.assets/climbing_stairs_dp.png" /></p>
|
||||
<p align="center"> Fig. 爬楼梯的动态规划过程 </p>
|
||||
|
||||
<h2 id="1414">14.1.4. 状态压缩<a class="headerlink" href="#1414" title="Permanent link">¶</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>,而只需两个变量滚动前进即可。如以下代码所示,由于省去了数组 <code>dp</code> 占用的空间,因此空间复杂度从 <span class="arithmatex">\(O(n)\)</span> 降低至 <span class="arithmatex">\(O(1)\)</span> 。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="5:11"><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" /><div class="tabbed-labels"><label for="__tabbed_5_1">Java</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Python</label><label for="__tabbed_5_4">Go</label><label for="__tabbed_5_5">JavaScript</label><label for="__tabbed_5_6">TypeScript</label><label for="__tabbed_5_7">C</label><label for="__tabbed_5_8">C#</label><label for="__tabbed_5_9">Swift</label><label for="__tabbed_5_10">Zig</label><label for="__tabbed_5_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -4155,12 +4200,6 @@ dp[i] = dp[i-1] + dp[i-2]
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>我们将这种空间优化技巧称为「状态压缩」</strong>。在许多动态规划问题中,当前状态仅与前面有限个状态有关,不必保存所有的历史状态,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。</p>
|
||||
<p>总的看来,<strong>子问题分解是一种通用的算法思路,在分治、动态规划、回溯中各有特点</strong>:</p>
|
||||
<ul>
|
||||
<li>分治算法将原问题划分为几个独立的子问题,然后递归解决子问题,最后合并子问题的解得到原问题的解。例如,归并排序将长数组不断划分为两个短子数组,再将排序好的子数组合并为排序好的长数组。</li>
|
||||
<li>动态规划也是将原问题分解为多个子问题,但与分治算法的主要区别是,<strong>动态规划中的子问题往往不是相互独立的</strong>,原问题的解依赖于子问题的解,而子问题的解又依赖于更小的子问题的解。</li>
|
||||
<li>回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作为一个子问题。</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2832,22 +2832,29 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1441" class="md-nav__link">
|
||||
14.4.1. 方法一:暴力搜索
|
||||
<a href="#_1" class="md-nav__link">
|
||||
方法一:暴力搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1442" class="md-nav__link">
|
||||
14.4.2. 方法二:记忆化搜索
|
||||
<a href="#_2" class="md-nav__link">
|
||||
方法二:记忆化搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1443" class="md-nav__link">
|
||||
14.4.3. 方法三:动态规划
|
||||
<a href="#_3" class="md-nav__link">
|
||||
方法三:动态规划
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_4" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
@@ -2967,6 +2974,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3102,6 +3111,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -3292,22 +3329,29 @@
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1441" class="md-nav__link">
|
||||
14.4.1. 方法一:暴力搜索
|
||||
<a href="#_1" class="md-nav__link">
|
||||
方法一:暴力搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1442" class="md-nav__link">
|
||||
14.4.2. 方法二:记忆化搜索
|
||||
<a href="#_2" class="md-nav__link">
|
||||
方法二:记忆化搜索
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#1443" class="md-nav__link">
|
||||
14.4.3. 方法三:动态规划
|
||||
<a href="#_3" class="md-nav__link">
|
||||
方法三:动态规划
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_4" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
@@ -3370,7 +3414,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
<p class="admonition-title">Tip</p>
|
||||
<p>完成以上三步后,我们可以直接实现从底至顶的动态规划解法。而为了展示本题包含的重叠子问题,本文也同时给出从顶至底的暴力搜索和记忆化搜索解法。</p>
|
||||
</div>
|
||||
<h2 id="1441">14.4.1. 方法一:暴力搜索<a class="headerlink" href="#1441" title="Permanent link">¶</a></h2>
|
||||
<h3 id="_1">方法一:暴力搜索<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
||||
<p>搜索代码包含以下要素:</p>
|
||||
<ul>
|
||||
<li><strong>递归参数</strong>:状态 <span class="arithmatex">\([i, c]\)</span> ;<strong>返回值</strong>:子问题的解 <span class="arithmatex">\(dp[i, c]\)</span> 。</li>
|
||||
@@ -3517,7 +3561,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
<p><img alt="0-1 背包的暴力搜索递归树" src="../knapsack_problem.assets/knapsack_dfs.png" /></p>
|
||||
<p align="center"> Fig. 0-1 背包的暴力搜索递归树 </p>
|
||||
|
||||
<h2 id="1442">14.4.2. 方法二:记忆化搜索<a class="headerlink" href="#1442" title="Permanent link">¶</a></h2>
|
||||
<h3 id="_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>
|
||||
<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">
|
||||
@@ -3689,7 +3733,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
<p><img alt="0-1 背包的记忆化搜索递归树" src="../knapsack_problem.assets/knapsack_dfs_mem.png" /></p>
|
||||
<p align="center"> Fig. 0-1 背包的记忆化搜索递归树 </p>
|
||||
|
||||
<h2 id="1443">14.4.3. 方法三:动态规划<a class="headerlink" href="#1443" title="Permanent link">¶</a></h2>
|
||||
<h3 id="_3">方法三:动态规划<a class="headerlink" href="#_3" title="Permanent link">¶</a></h3>
|
||||
<p>动态规划解法本质上就是在状态转移中填充 <span class="arithmatex">\(dp\)</span> 表的过程,代码如下所示。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="3:11"><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" /><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">JavaScript</label><label for="__tabbed_3_6">TypeScript</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></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -3890,7 +3934,8 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>最后考虑状态压缩</strong>。以上代码中的数组 <code>dp</code> 占用 <span class="arithmatex">\(O(n \times cap)\)</span> 空间。由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 <span class="arithmatex">\(O(n^2)\)</span> 将低至 <span class="arithmatex">\(O(n)\)</span> 。代码省略,有兴趣的同学可以自行实现。</p>
|
||||
<h3 id="_4">状态压缩<a class="headerlink" href="#_4" title="Permanent link">¶</a></h3>
|
||||
<p>最后考虑状态压缩。以上代码中的数组 <code>dp</code> 占用 <span class="arithmatex">\(O(n \times cap)\)</span> 空间。由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 <span class="arithmatex">\(O(n^2)\)</span> 将低至 <span class="arithmatex">\(O(n)\)</span> 。代码省略,有兴趣的同学可以自行实现。</p>
|
||||
<p>那么,我们是否可以仅用一个数组实现状态压缩呢?观察可知,每个状态都是由正上方或左上方的格子转移过来的。假设只有一个数组,当遍历到第 <span class="arithmatex">\(i\)</span> 行时,该数组存储的仍然是第 <span class="arithmatex">\(i-1\)</span> 行的状态,<strong>为了避免左方区域的格子在状态转移中被覆盖,应该采取倒序遍历</strong>。</p>
|
||||
<p>以下动画展示了在单个数组下从第 <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>
|
||||
|
||||
@@ -2908,6 +2908,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3043,6 +3045,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -3253,11 +3283,17 @@
|
||||
<li>不考虑时间的前提下,所有动态规划问题都可以用回溯(暴力搜索)进行求解,但递归树中存在大量的重叠子问题,效率极低。通过引入记忆化列表,可以存储所有计算过的子问题的解,从而保证重叠子问题只被计算一次。</li>
|
||||
<li>记忆化递归是一种从顶至底的递归式解法,而与之对应的动态规划是一种从底至顶的递推式解法,就像是在“填写表格”一样。由于当前状态仅依赖于某些局部状态,因此我们可以消除 <span class="arithmatex">\(dp\)</span> 表的一个维度,从而降低空间复杂度。</li>
|
||||
<li>动态规划问题的三大特性:重叠子问题、最优子结构、无后效性。如果原问题的最优解可以从子问题的最优解构建得来,则此问题就具有最优子结构。无后效性指对于一个状态,其未来发展只与该状态有关,与其所经历的过去的所有状态无关。许多组合优化问题都不具有无后效性,无法使用动态规划快速求解。</li>
|
||||
</ul>
|
||||
<p><strong>背包问题</strong></p>
|
||||
<ul>
|
||||
<li>背包问题是最典型的动态规划题目,具有 0-1 背包、完全背包、多重背包等变种问题。</li>
|
||||
<li>0-1 背包的状态定义为前 <span class="arithmatex">\(i\)</span> 个物品在剩余容量为 <span class="arithmatex">\(c\)</span> 的背包中的最大价值。这是一种常见的定义方式。不放入物品 <span class="arithmatex">\(i\)</span> ,状态转移至 <span class="arithmatex">\([i-1, c]\)</span> ,放入则转移至 <span class="arithmatex">\([i-1, c-wgt[i-1]]\)</span> ,由此便得到最优子结构,并构建出状态转移方程。对于状态压缩,由于每个状态依赖正上方和左上方的状态,因此需要倒序遍历列表,避免左上方状态被覆盖。</li>
|
||||
<li>完全背包的每种物品有无数个,因此在放置物品 <span class="arithmatex">\(i\)</span> 后,状态转移至 <span class="arithmatex">\([i, c-wgt[i-1]]\)</span> 。由于状态依赖于正上方和正左方的状态,因此状态压缩后应该正序遍历。</li>
|
||||
<li>零钱兑换问题是完全背包的一个变种。为从求“最大“价值变为求“最小”硬币数量,我们将状态转移方程中的 <span class="arithmatex">\(\max()\)</span> 改为 <span class="arithmatex">\(\min()\)</span> 。为从求“不超过”背包容量到求“恰好”凑出目标金额,我们使用 <span class="arithmatex">\(amt + 1\)</span> 来表示“无法凑出目标金额”的无效解。</li>
|
||||
<li>零钱兑换 II 问题从求“最少硬币数量”改为求“硬币组合数量”,状态转移方程相应地从 <span class="arithmatex">\(\min()\)</span> 改为求和运算符。</li>
|
||||
</ul>
|
||||
<p><strong>编辑距离问题</strong></p>
|
||||
<ul>
|
||||
<li>编辑距离(Levenshtein 距离)用于衡量两个字符串之间的相似度,定义为从一个字符串到另一个字符串的最小编辑步数,编辑操作包括添加、删除、替换。</li>
|
||||
<li>编辑距离问题的状态定义为将 <span class="arithmatex">\(s\)</span> 的前 <span class="arithmatex">\(i\)</span> 个字符更改为 <span class="arithmatex">\(t\)</span> 的前 <span class="arithmatex">\(j\)</span> 个字符所需的最少编辑步数。考虑字符 <span class="arithmatex">\(s[i]\)</span> 和 <span class="arithmatex">\(t[j]\)</span> ,具有三种决策:在 <span class="arithmatex">\(s[i-1]\)</span> 之后添加 <span class="arithmatex">\(t[j-1]\)</span> 、删除 <span class="arithmatex">\(s[i-1]\)</span> 、将 <span class="arithmatex">\(s[i-1]\)</span> 替换为 <span class="arithmatex">\(t[j-1]\)</span> ,它们都有相应的剩余子问题,据此就可以找出最优子结构与构建状态转移方程。值得注意的是,当 <span class="arithmatex">\(s[i] = t[j]\)</span> 时,无需编辑当前字符,直接跳过即可。</li>
|
||||
<li>在编辑距离中,状态依赖于其正上方、正左方、左上方的状态,因此状态压缩后正序或倒序遍历都无法正确地进行状态转移。利用一个变量暂存左上方状态,即转化至完全背包地情况,可以在状态压缩后使用正序遍历。</li>
|
||||
|
||||
@@ -2864,6 +2864,26 @@
|
||||
14.5.1. 完全背包问题
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.5.1. 完全背包问题">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_2" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -2871,6 +2891,26 @@
|
||||
14.5.2. 零钱兑换问题
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.5.2. 零钱兑换问题">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_3" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_4" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -2878,6 +2918,26 @@
|
||||
14.5.3. 零钱兑换问题 II
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.5.3. 零钱兑换问题 II">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_5" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_6" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
@@ -2967,6 +3027,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3102,6 +3164,34 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../chapter_greedy/max_product_cutting_problem/" class="md-nav__link">
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
15.4. 最大切分乘积问题
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
<span class="md-status md-status--new" title="最近添加">
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -3296,6 +3386,26 @@
|
||||
14.5.1. 完全背包问题
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.5.1. 完全背包问题">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_1" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_2" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -3303,6 +3413,26 @@
|
||||
14.5.2. 零钱兑换问题
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.5.2. 零钱兑换问题">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_3" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_4" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
@@ -3310,6 +3440,26 @@
|
||||
14.5.3. 零钱兑换问题 II
|
||||
</a>
|
||||
|
||||
<nav class="md-nav" aria-label="14.5.3. 零钱兑换问题 II">
|
||||
<ul class="md-nav__list">
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_5" class="md-nav__link">
|
||||
代码实现
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#_6" class="md-nav__link">
|
||||
状态压缩
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
@@ -3359,6 +3509,7 @@
|
||||
<div class="arithmatex">\[
|
||||
dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
\]</div>
|
||||
<h3 id="_1">代码实现<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
||||
<p>对比两道题目的动态规划代码,状态转移中有一处从 <span class="arithmatex">\(i-1\)</span> 变为 <span class="arithmatex">\(i\)</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">
|
||||
@@ -3512,6 +3663,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="_2">状态压缩<a class="headerlink" href="#_2" title="Permanent link">¶</a></h3>
|
||||
<p>由于当前状态是从左边和上边的状态转移而来,<strong>因此状态压缩后应该对 <span class="arithmatex">\(dp\)</span> 表中的每一行采取正序遍历</strong>,这个遍历顺序与 0-1 背包正好相反。请通过以下动画来理解为什么要改为正序遍历。</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">
|
||||
@@ -3719,7 +3871,8 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
|
||||
\]</div>
|
||||
<p><strong>第三步:确定边界条件和状态转移顺序</strong></p>
|
||||
<p>当目标金额为 <span class="arithmatex">\(0\)</span> 时,凑出它的最少硬币个数为 <span class="arithmatex">\(0\)</span> ,即所有 <span class="arithmatex">\(dp[i, 0]\)</span> 都等于 <span class="arithmatex">\(0\)</span> 。当无硬币时,<strong>无法凑出任意 <span class="arithmatex">\(> 0\)</span> 的目标金额</strong>,即是无效解。为使状态转移方程中的 <span class="arithmatex">\(\min()\)</span> 函数能够识别并过滤无效解,我们考虑使用 <span class="arithmatex">\(+ \infty\)</span> 来表示它们,即令所有 <span class="arithmatex">\(dp[0, a]\)</span> 都等于 <span class="arithmatex">\(+ \infty\)</span> 。</p>
|
||||
<p>以上做法仅适用于 Python 语言,因为大多数编程语言并未提供 <span class="arithmatex">\(+ \infty\)</span> 变量,所以只能使用整型 <code>int</code> 的最大值,而这又会导致大数越界:<strong>当 <span class="arithmatex">\(dp[i, a - coins[i-1]]\)</span> 是无效解时,再执行 <span class="arithmatex">\(+ 1\)</span> 操作会发生溢出</strong>。</p>
|
||||
<h3 id="_3">代码实现<a class="headerlink" href="#_3" title="Permanent link">¶</a></h3>
|
||||
<p>然而,大多数编程语言并未提供 <span class="arithmatex">\(+ \infty\)</span> 变量,因此只能使用整型 <code>int</code> 的最大值来代替,而这又会导致大数越界:<strong>当 <span class="arithmatex">\(dp[i, a - coins[i-1]]\)</span> 是无效解时,再执行 <span class="arithmatex">\(+ 1\)</span> 操作会发生溢出</strong>。</p>
|
||||
<p>为解决该问题,我们采用一个不可能达到的大数字 <span class="arithmatex">\(amt + 1\)</span> 来表示无效解,因为凑出 <span class="arithmatex">\(amt\)</span> 的硬币个数最多为 <span class="arithmatex">\(amt\)</span> 个。</p>
|
||||
<p>在最后返回前,判断 <span class="arithmatex">\(dp[n, amt]\)</span> 是否等于 <span class="arithmatex">\(amt + 1\)</span> ,若是则返回 <span class="arithmatex">\(-1\)</span> ,代表无法凑出目标金额。</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>
|
||||
@@ -3957,6 +4110,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="_4">状态压缩<a class="headerlink" href="#_4" title="Permanent link">¶</a></h3>
|
||||
<p>由于零钱兑换和完全背包的状态转移方程如出一辙,因此状态压缩方式也相同。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="6:11"><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" /><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">JavaScript</label><label for="__tabbed_6_6">TypeScript</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></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -4144,6 +4298,7 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
|
||||
dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
|
||||
\]</div>
|
||||
<p>当目标金额为 <span class="arithmatex">\(0\)</span> 时,无需选择任何硬币即可凑出目标金额,因此应将所有 <span class="arithmatex">\(dp[i, 0]\)</span> 都初始化为 <span class="arithmatex">\(1\)</span> 。当无硬币时,无法凑出任何 <span class="arithmatex">\(>0\)</span> 的目标金额,因此所有 <span class="arithmatex">\(dp[0, a]\)</span> 都等于 <span class="arithmatex">\(0\)</span> 。</p>
|
||||
<h3 id="_5">代码实现<a class="headerlink" href="#_5" title="Permanent link">¶</a></h3>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="7:11"><input checked="checked" id="__tabbed_7_1" name="__tabbed_7" type="radio" /><input id="__tabbed_7_2" name="__tabbed_7" type="radio" /><input id="__tabbed_7_3" name="__tabbed_7" type="radio" /><input id="__tabbed_7_4" name="__tabbed_7" type="radio" /><input id="__tabbed_7_5" name="__tabbed_7" type="radio" /><input id="__tabbed_7_6" name="__tabbed_7" type="radio" /><input id="__tabbed_7_7" name="__tabbed_7" type="radio" /><input id="__tabbed_7_8" name="__tabbed_7" type="radio" /><input id="__tabbed_7_9" name="__tabbed_7" type="radio" /><input id="__tabbed_7_10" name="__tabbed_7" type="radio" /><input id="__tabbed_7_11" name="__tabbed_7" type="radio" /><div class="tabbed-labels"><label for="__tabbed_7_1">Java</label><label for="__tabbed_7_2">C++</label><label for="__tabbed_7_3">Python</label><label for="__tabbed_7_4">Go</label><label for="__tabbed_7_5">JavaScript</label><label for="__tabbed_7_6">TypeScript</label><label for="__tabbed_7_7">C</label><label for="__tabbed_7_8">C#</label><label for="__tabbed_7_9">Swift</label><label for="__tabbed_7_10">Zig</label><label for="__tabbed_7_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -4319,6 +4474,7 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="_6">状态压缩<a class="headerlink" href="#_6" title="Permanent link">¶</a></h3>
|
||||
<p>状态压缩处理方式相同,删除硬币维度即可。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="8:11"><input checked="checked" id="__tabbed_8_1" name="__tabbed_8" type="radio" /><input id="__tabbed_8_2" name="__tabbed_8" type="radio" /><input id="__tabbed_8_3" name="__tabbed_8" type="radio" /><input id="__tabbed_8_4" name="__tabbed_8" type="radio" /><input id="__tabbed_8_5" name="__tabbed_8" type="radio" /><input id="__tabbed_8_6" name="__tabbed_8" type="radio" /><input id="__tabbed_8_7" name="__tabbed_8" type="radio" /><input id="__tabbed_8_8" name="__tabbed_8" type="radio" /><input id="__tabbed_8_9" name="__tabbed_8" type="radio" /><input id="__tabbed_8_10" name="__tabbed_8" type="radio" /><input id="__tabbed_8_11" name="__tabbed_8" type="radio" /><div class="tabbed-labels"><label for="__tabbed_8_1">Java</label><label for="__tabbed_8_2">C++</label><label for="__tabbed_8_3">Python</label><label for="__tabbed_8_4">Go</label><label for="__tabbed_8_5">JavaScript</label><label for="__tabbed_8_6">TypeScript</label><label for="__tabbed_8_7">C</label><label for="__tabbed_8_8">C#</label><label for="__tabbed_8_9">Swift</label><label for="__tabbed_8_10">Zig</label><label for="__tabbed_8_11">Dart</label></div>
|
||||
<div class="tabbed-content">
|
||||
|
||||
Reference in New Issue
Block a user