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

@@ -3440,7 +3440,7 @@
<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="../fractional_knapsack_problem.assets/fractional_knapsack_example.png" /></p>
<p align="center"> Fig. 分数背包问题的示例数据 </p>
<p align="center"> 图:分数背包问题的示例数据 </p>
<p>本题和 0-1 背包整体上非常相似,状态包含当前物品 <span class="arithmatex">\(i\)</span> 和容量 <span class="arithmatex">\(c\)</span> ,目标是求不超过背包容量下的最大价值。</p>
<p>不同点在于,本题允许只选择物品的一部分,<strong>这意味着可以对物品任意地进行切分,并按照重量比例来计算物品价值</strong>,因此有:</p>
@@ -3449,7 +3449,7 @@
<li>假设放入一部分物品 <span class="arithmatex">\(i\)</span> ,重量为 <span class="arithmatex">\(w\)</span> ,则背包增加的价值为 <span class="arithmatex">\(w \times val[i-1] / wgt[i-1]\)</span></li>
</ol>
<p><img alt="物品在单位重量下的价值" src="../fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png" /></p>
<p align="center"> Fig. 物品在单位重量下的价值 </p>
<p align="center"> 图:物品在单位重量下的价值 </p>
<h3 id="_1">贪心策略确定<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<p>最大化背包内物品总价值,<strong>本质上是要最大化单位重量下的物品价值</strong>。由此便可推出本题的贪心策略:</p>
@@ -3459,7 +3459,7 @@
<li>若剩余背包容量不足,则使用当前物品的一部分填满背包即可。</li>
</ol>
<p><img alt="分数背包的贪心策略" src="../fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png" /></p>
<p align="center"> Fig. 分数背包的贪心策略 </p>
<p align="center"> 图:分数背包的贪心策略 </p>
<h3 id="_2">代码实现<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<p>我们建立了一个物品类 <code>Item</code> ,以便将物品按照单位价值进行排序。循环进行贪心选择,当背包已满时跳出并返回解。</p>
@@ -3765,7 +3765,7 @@
<p>如下图所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。</p>
<p>通过这个类比,我们可以从几何角度理解贪心策略的有效性。</p>
<p><img alt="分数背包问题的几何表示" src="../fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png" /></p>
<p align="center"> Fig. 分数背包问题的几何表示 </p>
<p align="center"> 图:分数背包问题的几何表示 </p>

View File

@@ -3461,7 +3461,7 @@
</div>
<p>这道题的贪心策略在生活中很常见:给定目标金额,<strong>我们贪心地选择不大于且最接近它的硬币</strong>,不断循环该步骤,直至凑出目标金额为止。</p>
<p><img alt="零钱兑换的贪心策略" src="../greedy_algorithm.assets/coin_change_greedy_strategy.png" /></p>
<p align="center"> Fig. 零钱兑换的贪心策略 </p>
<p align="center"> 图:零钱兑换的贪心策略 </p>
<p>实现代码如下所示。你可能会不由地发出感叹So Clean !贪心算法仅用十行代码就解决了零钱兑换问题。</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>
@@ -3648,7 +3648,7 @@
<li><strong>反例 <span class="arithmatex">\(coins = [1, 49, 50]\)</span></strong>:假设 <span class="arithmatex">\(amt = 98\)</span> ,贪心算法只能找到 <span class="arithmatex">\(50 + 1 \times 48\)</span> 的兑换组合,共计 <span class="arithmatex">\(49\)</span> 枚硬币,但动态规划可以找到最优解 <span class="arithmatex">\(49 + 49\)</span> ,仅需 <span class="arithmatex">\(2\)</span> 枚硬币。</li>
</ul>
<p><img alt="贪心无法找出最优解的示例" src="../greedy_algorithm.assets/coin_change_greedy_vs_dp.png" /></p>
<p align="center"> Fig. 贪心无法找出最优解的示例 </p>
<p align="center"> 图:贪心无法找出最优解的示例 </p>
<p>也就是说,对于零钱兑换问题,贪心算法无法保证找到全局最优解,并且有可能找到非常差的解。它更适合用动态规划解决。</p>
<p>一般情况下,贪心算法适用于以下两类问题:</p>

View File

@@ -3441,7 +3441,7 @@
<p>请在数组中选择两个隔板,使得组成的容器的容量最大,返回最大容量。</p>
</div>
<p><img alt="最大容量问题的示例数据" src="../max_capacity_problem.assets/max_capacity_example.png" /></p>
<p align="center"> Fig. 最大容量问题的示例数据 </p>
<p align="center"> 图:最大容量问题的示例数据 </p>
<p>容器由任意两个隔板围成,<strong>因此本题的状态为两个隔板的索引,记为 <span class="arithmatex">\([i, j]\)</span></strong></p>
<p>根据题意,容量等于高度乘以宽度,其中高度由短板决定,宽度是两隔板的索引之差。设容量为 <span class="arithmatex">\(cap[i, j]\)</span> ,则可得计算公式:</p>
@@ -3452,7 +3452,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
<h3 id="_1">贪心策略确定<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<p>这道题还有更高效率的解法。如下图所示,现选取一个状态 <span class="arithmatex">\([i, j]\)</span> ,其满足索引 <span class="arithmatex">\(i &lt; j\)</span> 且高度 <span class="arithmatex">\(ht[i] &lt; ht[j]\)</span> ,即 <span class="arithmatex">\(i\)</span> 为短板、 <span class="arithmatex">\(j\)</span> 为长板。</p>
<p><img alt="初始状态" src="../max_capacity_problem.assets/max_capacity_initial_state.png" /></p>
<p align="center"> Fig. 初始状态 </p>
<p align="center"> 图:初始状态 </p>
<p>我们发现,<strong>如果此时将长板 <span class="arithmatex">\(j\)</span> 向短板 <span class="arithmatex">\(i\)</span> 靠近,则容量一定变小</strong>。这是因为在移动长板 <span class="arithmatex">\(j\)</span> 后:</p>
<ul>
@@ -3460,11 +3460,11 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
<li>高度由短板决定,因此高度只可能不变( <span class="arithmatex">\(i\)</span> 仍为短板)或变小(移动后的 <span class="arithmatex">\(j\)</span> 成为短板)。</li>
</ul>
<p><img alt="向内移动长板后的状态" src="../max_capacity_problem.assets/max_capacity_moving_long_board.png" /></p>
<p align="center"> Fig. 向内移动长板后的状态 </p>
<p align="center"> 图:向内移动长板后的状态 </p>
<p>反向思考,<strong>我们只有向内收缩短板 <span class="arithmatex">\(i\)</span> ,才有可能使容量变大</strong>。因为虽然宽度一定变小,<strong>但高度可能会变大</strong>(移动后的短板 <span class="arithmatex">\(i\)</span> 可能会变长)。</p>
<p><img alt="向内移动长板后的状态" src="../max_capacity_problem.assets/max_capacity_moving_short_board.png" /></p>
<p align="center"> Fig. 向内移动长板后的状态 </p>
<p align="center"> 图:向内移动长板后的状态 </p>
<p>由此便可推出本题的贪心策略:</p>
<ol>
@@ -3504,6 +3504,8 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
</div>
</div>
</div>
<p align="center"> 图:最大容量问题的贪心过程 </p>
<h3 id="_2">代码实现<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<p>代码循环最多 <span class="arithmatex">\(n\)</span> 轮,<strong>因此时间复杂度为 <span class="arithmatex">\(O(n)\)</span></strong></p>
<p>变量 <span class="arithmatex">\(i\)</span> , <span class="arithmatex">\(j\)</span> , <span class="arithmatex">\(res\)</span> 使用常数大小额外空间,<strong>因此空间复杂度为 <span class="arithmatex">\(O(1)\)</span></strong></p>
@@ -3697,7 +3699,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
cap[i, i+1], cap[i, i+2], \cdots, cap[i, j-2], cap[i, j-1]
\]</div>
<p><img alt="移动短板导致被跳过的状态" src="../max_capacity_problem.assets/max_capacity_skipped_states.png" /></p>
<p align="center"> Fig. 移动短板导致被跳过的状态 </p>
<p align="center"> 图:移动短板导致被跳过的状态 </p>
<p>观察发现,<strong>这些被跳过的状态实际上就是将长板 <span class="arithmatex">\(j\)</span> 向内移动的所有状态</strong>。而在第二步中,我们已经证明内移长板一定会导致容量变小。也就是说,被跳过的状态都不可能是最优解,<strong>跳过它们不会导致错过最优解</strong></p>
<p>以上的分析说明,<strong>移动短板的操作是“安全”的</strong>,贪心策略是有效的。</p>

View File

@@ -3439,7 +3439,7 @@
<p>给定一个正整数 <span class="arithmatex">\(n\)</span> ,将其切分为至少两个正整数的和,求切分后所有整数的乘积最大是多少。</p>
</div>
<p><img alt="最大切分乘积的问题定义" src="../max_product_cutting_problem.assets/max_product_cutting_definition.png" /></p>
<p align="center"> Fig. 最大切分乘积的问题定义 </p>
<p align="center"> 图:最大切分乘积的问题定义 </p>
<p>假设我们将 <span class="arithmatex">\(n\)</span> 切分为 <span class="arithmatex">\(m\)</span> 个整数因子,其中第 <span class="arithmatex">\(i\)</span> 个因子记为 <span class="arithmatex">\(n_i\)</span> ,即</p>
<div class="arithmatex">\[
@@ -3462,13 +3462,13 @@ n &amp; \geq 4
<p>我们发现当 <span class="arithmatex">\(n \geq 4\)</span> 时,切分出一个 <span class="arithmatex">\(2\)</span> 后乘积会变大,<strong>这说明大于等于 <span class="arithmatex">\(4\)</span> 的整数都应该被切分</strong></p>
<p><strong>贪心策略一</strong>:如果切分方案中包含 <span class="arithmatex">\(\geq 4\)</span> 的因子,那么它就应该被继续切分。最终的切分方案只应出现 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(3\)</span> 这三种因子。</p>
<p><img alt="切分导致乘积变大" src="../max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png" /></p>
<p align="center"> Fig. 切分导致乘积变大 </p>
<p align="center"> 图:切分导致乘积变大 </p>
<p>接下来思考哪个因子是最优的。在 <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(3\)</span> 这三个因子中,显然 <span class="arithmatex">\(1\)</span> 是最差的,因为 <span class="arithmatex">\(1 \times (n-1) &lt; n\)</span> 恒成立,即切分出 <span class="arithmatex">\(1\)</span> 反而会导致乘积减小。</p>
<p>我们发现,当 <span class="arithmatex">\(n = 6\)</span> 时,有 <span class="arithmatex">\(3 \times 3 &gt; 2 \times 2 \times 2\)</span><strong>这意味着切分出 <span class="arithmatex">\(3\)</span> 比切分出 <span class="arithmatex">\(2\)</span> 更优</strong></p>
<p><strong>贪心策略二</strong>:在切分方案中,最多只应存在两个 <span class="arithmatex">\(2\)</span> 。因为三个 <span class="arithmatex">\(2\)</span> 总是可以被替换为两个 <span class="arithmatex">\(3\)</span> ,从而获得更大乘积。</p>
<p><img alt="最优切分因子" src="../max_product_cutting_problem.assets/max_product_cutting_greedy_infer3.png" /></p>
<p align="center"> Fig. 最优切分因子 </p>
<p align="center"> 图:最优切分因子 </p>
<p>总结以上,可推出贪心策略:</p>
<ol>
@@ -3664,7 +3664,7 @@ n = 3 a + b
</div>
</div>
<p><img alt="最大切分乘积的计算方法" src="../max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png" /></p>
<p align="center"> Fig. 最大切分乘积的计算方法 </p>
<p align="center"> 图:最大切分乘积的计算方法 </p>
<p><strong>时间复杂度取决于编程语言的幂运算的实现方法</strong>。以 Python 为例,常用的幂计算函数有三种:</p>
<ul>