This commit is contained in:
krahets
2023-08-21 19:32:49 +08:00
parent c0f960b443
commit c359c07fe0
67 changed files with 443 additions and 442 deletions
@@ -3434,7 +3434,6 @@
<h1 id="152">15.2 &nbsp; 分数背包问题<a class="headerlink" href="#152" title="Permanent link">&para;</a></h1>
<p>分数背包是 0-1 背包的一个变种问题。</p>
<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>
@@ -3442,8 +3441,8 @@
<p><img alt="分数背包问题的示例数据" src="../fractional_knapsack_problem.assets/fractional_knapsack_example.png" /></p>
<p align="center"> 图:分数背包问题的示例数据 </p>
<p>本题和 0-1 背包整体上非常相似,状态包含当前物品 <span class="arithmatex">\(i\)</span> 和容量 <span class="arithmatex">\(c\)</span> ,目标是求不超过背包容量下的最大价值。</p>
<p>不同点在于,本题允许只选择物品的一部分,<strong>这意味着可以对物品任意地进行切分,并按照重量比例来计算物品价值</strong>,因此有:</p>
<p>分数背包和 0-1 背包整体上非常相似,状态包含当前物品 <span class="arithmatex">\(i\)</span> 和容量 <span class="arithmatex">\(c\)</span> ,目标是求不超过背包容量下的最大价值。</p>
<p>不同点在于,本题允许只选择物品的一部分。如下图所示<strong>我们可以对物品任意地进行切分,并按照重量比例来计算物品价值</strong></p>
<ol>
<li>对于物品 <span class="arithmatex">\(i\)</span> ,它在单位重量下的价值为 <span class="arithmatex">\(val[i-1] / wgt[i-1]\)</span> ,简称为单位价值。</li>
<li>假设放入一部分物品 <span class="arithmatex">\(i\)</span> ,重量为 <span class="arithmatex">\(w\)</span> ,则背包增加的价值为 <span class="arithmatex">\(w \times val[i-1] / wgt[i-1]\)</span></li>
@@ -3452,7 +3451,7 @@
<p align="center"> 图:物品在单位重量下的价值 </p>
<h3 id="1">1. &nbsp; 贪心策略确定<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>最大化背包内物品总价值,<strong>本质上是要最大化单位重量下的物品价值</strong>。由此便可推出本题的贪心策略:</p>
<p>最大化背包内物品总价值,<strong>本质上是要最大化单位重量下的物品价值</strong>。由此便可推出下图所示的贪心策略:</p>
<ol>
<li>将物品按照单位价值从高到低进行排序。</li>
<li>遍历所有物品,<strong>每轮贪心地选择单位价值最高的物品</strong></li>
@@ -3762,8 +3761,7 @@
<p>采用反证法。假设物品 <span class="arithmatex">\(x\)</span> 是单位价值最高的物品,使用某算法求得最大价值为 <code>res</code> ,但该解中不包含物品 <span class="arithmatex">\(x\)</span></p>
<p>现在从背包中拿出单位重量的任意物品,并替换为单位重量的物品 <span class="arithmatex">\(x\)</span> 。由于物品 <span class="arithmatex">\(x\)</span> 的单位价值最高,因此替换后的总价值一定大于 <code>res</code><strong>这与 <code>res</code> 是最优解矛盾,说明最优解中必须包含物品 <span class="arithmatex">\(x\)</span></strong></p>
<p>对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,<strong>单位价值更大的物品总是更优选择</strong>,这说明贪心策略是有效的。</p>
<p>如下图所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。</p>
<p>通过这个类比,我们可以从几何角度理解贪心策略的有效性。</p>
<p>如下图所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。</p>
<p><img alt="分数背包问题的几何表示" src="../fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png" /></p>
<p align="center"> 图:分数背包问题的几何表示 </p>
+2 -2
View File
@@ -3459,7 +3459,7 @@
<p class="admonition-title">Question</p>
<p>给定 <span class="arithmatex">\(n\)</span> 种硬币,第 <span class="arithmatex">\(i\)</span> 种硬币的面值为 <span class="arithmatex">\(coins[i - 1]\)</span> ,目标金额为 <span class="arithmatex">\(amt\)</span> ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 <span class="arithmatex">\(-1\)</span></p>
</div>
<p>这道题的贪心策略在生活中很常见:给定目标金额,<strong>我们贪心地选择不大于且最接近它的硬币</strong>,不断循环该步骤,直至凑出目标金额为止。</p>
<p>题的贪心策略如下图所示。给定目标金额,<strong>我们贪心地选择不大于且最接近它的硬币</strong>,不断循环该步骤,直至凑出目标金额为止。</p>
<p><img alt="零钱兑换的贪心策略" src="../greedy_algorithm.assets/coin_change_greedy_strategy.png" /></p>
<p align="center"> 图:零钱兑换的贪心策略 </p>
@@ -3641,7 +3641,7 @@
</div>
<h2 id="1511">15.1.1 &nbsp; 贪心优点与局限性<a class="headerlink" href="#1511" title="Permanent link">&para;</a></h2>
<p><strong>贪心算法不仅操作直接、实现简单,而且通常效率也很高</strong>。在以上代码中,记硬币最小面值为 <span class="arithmatex">\(\min(coins)\)</span> ,则贪心选择最多循环 <span class="arithmatex">\(amt / \min(coins)\)</span> 次,时间复杂度为 <span class="arithmatex">\(O(amt / \min(coins))\)</span> 。这比动态规划解法的时间复杂度 <span class="arithmatex">\(O(n \times amt)\)</span> 提升了一个数量级。</p>
<p>然而,<strong>对于某些硬币面值组合,贪心算法并不能找到最优解</strong>我们来看几个例子:</p>
<p>然而,<strong>对于某些硬币面值组合,贪心算法并不能找到最优解</strong>下图给出了两个示例。</p>
<ul>
<li><strong>正例 <span class="arithmatex">\(coins = [1, 5, 10, 20, 50, 100]\)</span></strong>:在该硬币组合下,给定任意 <span class="arithmatex">\(amt\)</span> ,贪心算法都可以找出最优解。</li>
<li><strong>反例 <span class="arithmatex">\(coins = [1, 20, 50]\)</span></strong>:假设 <span class="arithmatex">\(amt = 60\)</span> ,贪心算法只能找到 <span class="arithmatex">\(50 + 1 \times 10\)</span> 的兑换组合,共计 <span class="arithmatex">\(11\)</span> 枚硬币,但动态规划可以找到最优解 <span class="arithmatex">\(20 + 20 + 20\)</span> ,仅需 <span class="arithmatex">\(3\)</span> 枚硬币。</li>
@@ -3454,7 +3454,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
<p><img alt="初始状态" src="../max_capacity_problem.assets/max_capacity_initial_state.png" /></p>
<p align="center"> 图:初始状态 </p>
<p>我们发现<strong>如果此时将长板 <span class="arithmatex">\(j\)</span> 向短板 <span class="arithmatex">\(i\)</span> 靠近,则容量一定变小</strong>。这是因为在移动长板 <span class="arithmatex">\(j\)</span> 后:</p>
<p>如下图所示<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>
@@ -3462,11 +3462,12 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
<p><img alt="向内移动长板后的状态" src="../max_capacity_problem.assets/max_capacity_moving_long_board.png" /></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"> 图:向内移动板后的状态 </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"> 图:向内移动板后的状态 </p>
<p>由此便可推出本题的贪心策略:</p>
<p>由此便可推出本题的贪心策略:初始化两指针分裂容器两端,每轮向内收缩短板对应的指针,直至两指针相遇。</p>
<p>下图展示了贪心策略的执行过程。</p>
<ol>
<li>初始状态下,指针 <span class="arithmatex">\(i\)</span> , <span class="arithmatex">\(j\)</span> 分列与数组两端。</li>
<li>计算当前状态的容量 <span class="arithmatex">\(cap[i, j]\)</span> ,并更新最大容量。</li>
@@ -3694,7 +3695,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
</div>
<h3 id="3">3. &nbsp; 正确性证明<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。</p>
<p>比如在状态 <span class="arithmatex">\(cap[i, j]\)</span> 下,<span class="arithmatex">\(i\)</span> 为短板、<span class="arithmatex">\(j\)</span> 为长板。若贪心地将短板 <span class="arithmatex">\(i\)</span> 向内移动一格,会导致下状态被“跳过”。<strong>这意味着之后无法验证这些状态的容量大小</strong></p>
<p>比如在状态 <span class="arithmatex">\(cap[i, j]\)</span> 下,<span class="arithmatex">\(i\)</span> 为短板、<span class="arithmatex">\(j\)</span> 为长板。若贪心地将短板 <span class="arithmatex">\(i\)</span> 向内移动一格,会导致下图所示的状态被“跳过”。<strong>这意味着之后无法验证这些状态的容量大小</strong></p>
<div class="arithmatex">\[
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
\]</div>
@@ -3459,13 +3459,13 @@ n = \sum_{i=1}^{m}n_i
n &amp; \geq 4
\end{aligned}
\]</div>
<p>我们发现<span class="arithmatex">\(n \geq 4\)</span> 时,切分出一个 <span class="arithmatex">\(2\)</span> 后乘积会变大,<strong>这说明大于等于 <span class="arithmatex">\(4\)</span> 的整数都应该被切分</strong></p>
<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"> 图:切分导致乘积变大 </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>如下图所示,当 <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"> 图:最优切分因子 </p>
@@ -3478,7 +3478,7 @@ n &amp; \geq 4
<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>
</ol>
<h3 id="2">2. &nbsp; 代码实现<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>在代码中,我们无须通过循环来切分整数,而可以利用向下整除运算得到 <span class="arithmatex">\(3\)</span> 的个数 <span class="arithmatex">\(a\)</span> ,用取模运算得到余数 <span class="arithmatex">\(b\)</span> ,此时有:</p>
<p>如下图所示,我们无须通过循环来切分整数,而可以利用向下整除运算得到 <span class="arithmatex">\(3\)</span> 的个数 <span class="arithmatex">\(a\)</span> ,用取模运算得到余数 <span class="arithmatex">\(b\)</span> ,此时有:</p>
<div class="arithmatex">\[
n = 3 a + b
\]</div>
@@ -3666,7 +3666,7 @@ n = 3 a + b
<p><img alt="最大切分乘积的计算方法" src="../max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png" /></p>
<p align="center"> 图:最大切分乘积的计算方法 </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>