This commit is contained in:
krahets
2023-08-27 23:41:10 +08:00
parent 8c9cf3f087
commit 016f13d882
66 changed files with 262 additions and 270 deletions

View File

@@ -3466,7 +3466,7 @@
<h1 id="152">15.2 &nbsp; 分数背包问题<a class="headerlink" href="#152" title="Permanent link">&para;</a></h1>
<div class="admonition question">
<p class="admonition-title">Question</p>
<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>
<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"> 图 15-3 &nbsp; 分数背包问题的示例数据 </p>
@@ -3481,7 +3481,7 @@
<p align="center"> 图 15-4 &nbsp; 物品在单位重量下的价值 </p>
<h3 id="1">1. &nbsp; 贪心策略确定<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>最大化背包内物品总价值,<strong>本质上是要最大化单位重量下的物品价值</strong>。由此便可推出图 15-5 所示的贪心策略</p>
<p>最大化背包内物品总价值,<strong>本质上是要最大化单位重量下的物品价值</strong>。由此便可推出图 15-5 所示的贪心策略</p>
<ol>
<li>将物品按照单位价值从高到低进行排序。</li>
<li>遍历所有物品,<strong>每轮贪心地选择单位价值最高的物品</strong></li>

View File

@@ -3479,7 +3479,7 @@
<h1 id="151">15.1 &nbsp; 贪心算法<a class="headerlink" href="#151" title="Permanent link">&para;</a></h1>
<p>贪心算法是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期望获得全局最优解。贪心算法简洁且高效,在许多实际问题中都有着广泛的应用。</p>
<p>贪心算法和动态规划都常用于解决优化问题。它们一些相似之处,比如都依赖最优子结构性质。两者的不同点在于:</p>
<p>贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理是不同的。</p>
<ul>
<li>动态规划会根据之前阶段的所有决策来考虑当前决策,并使用过去子问题的解来构建当前子问题的解。</li>
<li>贪心算法不会重新考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决。</li>
@@ -3681,14 +3681,14 @@
<p align="center"> 图 15-2 &nbsp; 贪心无法找出最优解的示例 </p>
<p>也就是说,对于零钱兑换问题,贪心算法无法保证找到全局最优解,并且有可能找到非常差的解。它更适合用动态规划解决。</p>
<p>一般情况下,贪心算法适用于以下两类问题</p>
<p>一般情况下,贪心算法适用于以下两类问题</p>
<ol>
<li><strong>可以保证找到最优解</strong>:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效。</li>
<li><strong>可以找到近似最优解</strong>:贪心算法在这种情况下也是可用的。对于很多复杂问题来说,寻找全局最优解是非常困难的,能以较高效率找到次优解也是非常不错的。</li>
</ol>
<h2 id="1512">15.1.2 &nbsp; 贪心算法特性<a class="headerlink" href="#1512" title="Permanent link">&para;</a></h2>
<p>那么问题来了,什么样的问题适合用贪心算法求解呢?或者说,贪心算法在什么情况下可以保证找到最优解?</p>
<p>相较于动态规划,贪心算法的使用条件更加苛刻,其主要关注问题的两个性质</p>
<p>相较于动态规划,贪心算法的使用条件更加苛刻,其主要关注问题的两个性质</p>
<ul>
<li><strong>贪心选择性质</strong>:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证得到最优解。</li>
<li><strong>最优子结构</strong>:原问题的最优解包含子问题的最优解。</li>
@@ -3702,13 +3702,13 @@
<p>Pearson, David. A polynomial-time algorithm for the change-making problem. Operations Research Letters 33.3 (2005): 231-234.</p>
</div>
<h2 id="1513">15.1.3 &nbsp; 贪心解题步骤<a class="headerlink" href="#1513" title="Permanent link">&para;</a></h2>
<p>贪心问题的解决流程大体可分为三步</p>
<p>贪心问题的解决流程大体可分为以下三步</p>
<ol>
<li><strong>问题分析</strong>:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。这一步在回溯和动态规划中都有涉及。</li>
<li><strong>确定贪心策略</strong>:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终能解决整个问题。</li>
<li><strong>正确性证明</strong>:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要使用到数学证明,例如归纳法或反证法等。</li>
</ol>
<p>确定贪心策略是求解问题的核心步骤,但实施起来可能并不容易,原因包括:</p>
<p>确定贪心策略是求解问题的核心步骤,但实施起来可能并不容易,主要包含以下原因。</p>
<ul>
<li><strong>不同问题的贪心策略的差异较大</strong>。对于许多问题来说,贪心策略都比较浅显,我们通过一些大概的思考与尝试就能得出。而对于一些复杂问题,贪心策略可能非常隐蔽,这种情况就非常考验个人的解题经验与算法能力了。</li>
<li><strong>某些贪心策略具有较强的迷惑性</strong>。当我们满怀信心设计好贪心策略,写出解题代码并提交运行,很可能发现部分测试样例无法通过。这是因为设计的贪心策略只是“部分正确”的,上文介绍的零钱兑换就是个典型案例。</li>
@@ -3716,7 +3716,7 @@
<p>为了保证正确性,我们应该对贪心策略进行严谨的数学证明,<strong>通常需要用到反证法或数学归纳法</strong></p>
<p>然而,正确性证明也很可能不是一件易事。如若没有头绪,我们通常会选择面向测试用例进行 Debug ,一步步修改与验证贪心策略。</p>
<h2 id="1514">15.1.4 &nbsp; 贪心典型例题<a class="headerlink" href="#1514" title="Permanent link">&para;</a></h2>
<p>贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下一些典型的贪心算法问题</p>
<p>贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下列举了一些典型的贪心算法问题</p>
<ol>
<li><strong>硬币找零问题</strong>:在某些硬币组合下,贪心算法总是可以得到最优解。</li>
<li><strong>区间调度问题</strong>:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。</li>

View File

@@ -3480,15 +3480,12 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
\]</div>
<p>设数组长度为 <span class="arithmatex">\(n\)</span> ,两个隔板的组合数量(即状态总数)为 <span class="arithmatex">\(C_n^2 = \frac{n(n - 1)}{2}\)</span> 个。最直接地,<strong>我们可以穷举所有状态</strong>,从而求得最大容量,时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span></p>
<h3 id="1">1. &nbsp; 贪心策略确定<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>这道题还有更高效率的解法。如图 15-8 所示,现选取一个状态 <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>这道题还有更高效率的解法。如图 15-8 所示,现选取一个状态 <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"> 图 15-8 &nbsp; 初始状态 </p>
<p>如图 15-9 所示,<strong>若此时将长板 <span class="arithmatex">\(j\)</span> 向短板 <span class="arithmatex">\(i\)</span> 靠近,则容量一定变小</strong>这是因为在移动长板 <span class="arithmatex">\(j\)</span> 后:</p>
<ul>
<li>宽度 <span class="arithmatex">\(j-i\)</span> 肯定变小。</li>
<li>高度由短板决定,因此高度只可能不变( <span class="arithmatex">\(i\)</span> 仍为短板)或变小(移动后的 <span class="arithmatex">\(j\)</span> 成为短板)。</li>
</ul>
<p>如图 15-9 所示,<strong>若此时将长板 <span class="arithmatex">\(j\)</span> 向短板 <span class="arithmatex">\(i\)</span> 靠近,则容量一定变小</strong></p>
<p>这是因为在移动长板 <span class="arithmatex">\(j\)</span> 后,宽度 <span class="arithmatex">\(j-i\)</span> 肯定变小;而高度由短板决定,因此高度只可能不变( <span class="arithmatex">\(i\)</span> 仍为短板)或变小(移动后的 <span class="arithmatex">\(j\)</span> 成为短板)。</p>
<p><img alt="向内移动长板后的状态" src="../max_capacity_problem.assets/max_capacity_moving_long_board.png" /></p>
<p align="center"> 图 15-9 &nbsp; 向内移动长板后的状态 </p>
@@ -3499,10 +3496,10 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
<p>由此便可推出本题的贪心策略:初始化两指针分裂容器两端,每轮向内收缩短板对应的指针,直至两指针相遇。</p>
<p>图 15-11 展示了贪心策略的执行过程。</p>
<ol>
<li>初始状态下,指针 <span class="arithmatex">\(i\)</span> , <span class="arithmatex">\(j\)</span> 分列与数组两端。</li>
<li>初始状态下,指针 <span class="arithmatex">\(i\)</span> <span class="arithmatex">\(j\)</span> 分列与数组两端。</li>
<li>计算当前状态的容量 <span class="arithmatex">\(cap[i, j]\)</span> ,并更新最大容量。</li>
<li>比较板 <span class="arithmatex">\(i\)</span> 和 板 <span class="arithmatex">\(j\)</span> 的高度,并将短板向内移动一格。</li>
<li>循环执行第 <code>2.</code> , <code>3.</code> 步,直至 <span class="arithmatex">\(i\)</span><span class="arithmatex">\(j\)</span> 相遇时结束。</li>
<li>循环执行第 <code>2.</code> <code>3.</code> 步,直至 <span class="arithmatex">\(i\)</span><span class="arithmatex">\(j\)</span> 相遇时结束。</li>
</ol>
<div class="tabbed-set tabbed-alternate" data-tabs="1:9"><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" /><div class="tabbed-labels"><label for="__tabbed_1_1">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label><label for="__tabbed_1_5">&lt;5&gt;</label><label for="__tabbed_1_6">&lt;6&gt;</label><label for="__tabbed_1_7">&lt;7&gt;</label><label for="__tabbed_1_8">&lt;8&gt;</label><label for="__tabbed_1_9">&lt;9&gt;</label></div>
<div class="tabbed-content">
@@ -3539,7 +3536,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
<h3 id="2">2. &nbsp; 代码实现<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>
<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>
<div class="tabbed-set tabbed-alternate" data-tabs="2:12"><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" /><input id="__tabbed_2_12" 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">JS</label><label for="__tabbed_2_6">TS</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><label for="__tabbed_2_12">Rust</label></div>
<div class="tabbed-content">
<div class="tabbed-block">

View File

@@ -3490,19 +3490,19 @@ n &amp; \geq 4
\end{aligned}
\]</div>
<p>如图 15-14 所示,当 <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><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"> 图 15-14 &nbsp; 切分导致乘积变大 </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">\(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>如图 15-15 所示,当 <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"> 图 15-15 &nbsp; 最优切分因子 </p>
<p>总结以上,可推出贪心策略</p>
<p>总结以上,可推出以下贪心策略</p>
<ol>
<li>输入整数 <span class="arithmatex">\(n\)</span> ,从其不断地切分出因子 <span class="arithmatex">\(3\)</span> ,直至余数为 <span class="arithmatex">\(0\)</span> , <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(2\)</span></li>
<li>输入整数 <span class="arithmatex">\(n\)</span> ,从其不断地切分出因子 <span class="arithmatex">\(3\)</span> ,直至余数为 <span class="arithmatex">\(0\)</span><span class="arithmatex">\(1\)</span><span class="arithmatex">\(2\)</span></li>
<li>当余数为 <span class="arithmatex">\(0\)</span> 时,代表 <span class="arithmatex">\(n\)</span><span class="arithmatex">\(3\)</span> 的倍数,因此不做任何处理。</li>
<li>当余数为 <span class="arithmatex">\(2\)</span> 时,不继续划分,保留之。</li>
<li>当余数为 <span class="arithmatex">\(1\)</span> 时,由于 <span class="arithmatex">\(2 \times 2 &gt; 1 \times 3\)</span> ,因此应将最后一个 <span class="arithmatex">\(3\)</span> 替换为 <span class="arithmatex">\(2\)</span></li>
@@ -3696,12 +3696,12 @@ n = 3 a + b
<p><img alt="最大切分乘积的计算方法" src="../max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png" /></p>
<p align="center"> 图 15-16 &nbsp; 最大切分乘积的计算方法 </p>
<p><strong>时间复杂度取决于编程语言的幂运算的实现方法</strong>。以 Python 为例,常用的幂计算函数有</p>
<p><strong>时间复杂度取决于编程语言的幂运算的实现方法</strong>。以 Python 为例,常用的幂计算函数有三种。</p>
<ul>
<li>运算符 <code>**</code> 和函数 <code>pow()</code> 的时间复杂度均为 <span class="arithmatex">\(O(\log a)\)</span></li>
<li>函数 <code>math.pow()</code> 内部调用 C 语言库的 <code>pow()</code> 函数,其执行浮点取幂,时间复杂度为 <span class="arithmatex">\(O(1)\)</span></li>
</ul>
<p>变量 <span class="arithmatex">\(a\)</span> , <span class="arithmatex">\(b\)</span> 使用常数大小的额外空间,<strong>因此空间复杂度为 <span class="arithmatex">\(O(1)\)</span></strong></p>
<p>变量 <span class="arithmatex">\(a\)</span> <span class="arithmatex">\(b\)</span> 使用常数大小的额外空间,<strong>因此空间复杂度为 <span class="arithmatex">\(O(1)\)</span></strong></p>
<h3 id="3">3. &nbsp; 正确性证明<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>使用反证法,只分析 <span class="arithmatex">\(n \geq 3\)</span> 的情况。</p>
<ol>