This commit is contained in:
krahets
2023-08-22 13:50:12 +08:00
parent 0c9bf14e20
commit 92a0853ab8
64 changed files with 478 additions and 479 deletions
@@ -11,22 +11,22 @@ status: new
![分数背包问题的示例数据](fractional_knapsack_problem.assets/fractional_knapsack_example.png)
<p align="center"> 图分数背包问题的示例数据 </p>
<p align="center"> 图 15-3 &nbsp; 分数背包问题的示例数据 </p>
分数背包和 0-1 背包整体上非常相似,状态包含当前物品 $i$ 和容量 $c$ ,目标是求不超过背包容量下的最大价值。
不同点在于,本题允许只选择物品的一部分。如图所示,**我们可以对物品任意地进行切分,并按照重量比例来计算物品价值**。
不同点在于,本题允许只选择物品的一部分。如图 15-4 所示,**我们可以对物品任意地进行切分,并按照重量比例来计算物品价值**。
1. 对于物品 $i$ ,它在单位重量下的价值为 $val[i-1] / wgt[i-1]$ ,简称为单位价值。
2. 假设放入一部分物品 $i$ ,重量为 $w$ ,则背包增加的价值为 $w \times val[i-1] / wgt[i-1]$ 。
![物品在单位重量下的价值](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png)
<p align="center"> 图物品在单位重量下的价值 </p>
<p align="center"> 图 15-4 &nbsp; 物品在单位重量下的价值 </p>
### 1. &nbsp; 贪心策略确定
最大化背包内物品总价值,**本质上是要最大化单位重量下的物品价值**。由此便可推出图所示的贪心策略:
最大化背包内物品总价值,**本质上是要最大化单位重量下的物品价值**。由此便可推出图 15-5 所示的贪心策略:
1. 将物品按照单位价值从高到低进行排序。
2. 遍历所有物品,**每轮贪心地选择单位价值最高的物品**。
@@ -34,7 +34,7 @@ status: new
![分数背包的贪心策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png)
<p align="center"> 图分数背包的贪心策略 </p>
<p align="center"> 图 15-5 &nbsp; 分数背包的贪心策略 </p>
### 2. &nbsp; 代码实现
@@ -365,8 +365,8 @@ status: new
对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,**单位价值更大的物品总是更优选择**,这说明贪心策略是有效的。
图所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。
如图 15-6 所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。
![分数背包问题的几何表示](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png)
<p align="center"> 图分数背包问题的几何表示 </p>
<p align="center"> 图 15-6 &nbsp; 分数背包问题的几何表示 </p>
+4 -4
View File
@@ -18,11 +18,11 @@ status: new
给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 $-1$ 。
本题的贪心策略如图所示。给定目标金额,**我们贪心地选择不大于且最接近它的硬币**,不断循环该步骤,直至凑出目标金额为止。
本题的贪心策略如图 15-1 所示。给定目标金额,**我们贪心地选择不大于且最接近它的硬币**,不断循环该步骤,直至凑出目标金额为止。
![零钱兑换的贪心策略](greedy_algorithm.assets/coin_change_greedy_strategy.png)
<p align="center"> 图零钱兑换的贪心策略 </p>
<p align="center"> 图 15-1 &nbsp; 零钱兑换的贪心策略 </p>
实现代码如下所示。你可能会不由地发出感叹:So Clean !贪心算法仅用十行代码就解决了零钱兑换问题。
@@ -225,7 +225,7 @@ status: new
**贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times amt)$ 提升了一个数量级。
然而,**对于某些硬币面值组合,贪心算法并不能找到最优解**。图给出了两个示例。
然而,**对于某些硬币面值组合,贪心算法并不能找到最优解**。图 15-2 给出了两个示例。
- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:在该硬币组合下,给定任意 $amt$ ,贪心算法都可以找出最优解。
- **反例 $coins = [1, 20, 50]$**:假设 $amt = 60$ ,贪心算法只能找到 $50 + 1 \times 10$ 的兑换组合,共计 $11$ 枚硬币,但动态规划可以找到最优解 $20 + 20 + 20$ ,仅需 $3$ 枚硬币。
@@ -233,7 +233,7 @@ status: new
![贪心无法找出最优解的示例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png)
<p align="center"> 图贪心无法找出最优解的示例 </p>
<p align="center"> 图 15-2 &nbsp; 贪心无法找出最优解的示例 </p>
也就是说,对于零钱兑换问题,贪心算法无法保证找到全局最优解,并且有可能找到非常差的解。它更适合用动态规划解决。
+11 -11
View File
@@ -15,7 +15,7 @@ status: new
![最大容量问题的示例数据](max_capacity_problem.assets/max_capacity_example.png)
<p align="center"> 图最大容量问题的示例数据 </p>
<p align="center"> 图 15-7 &nbsp; 最大容量问题的示例数据 </p>
容器由任意两个隔板围成,**因此本题的状态为两个隔板的索引,记为 $[i, j]$** 。
@@ -29,30 +29,30 @@ $$
### 1. &nbsp; 贪心策略确定
这道题还有更高效率的解法。如图所示,现选取一个状态 $[i, j]$ ,其满足索引 $i < j$ 且高度 $ht[i] < ht[j]$ ,即 $i$ 为短板、 $j$ 为长板。
这道题还有更高效率的解法。如图 15-8 所示,现选取一个状态 $[i, j]$ ,其满足索引 $i < j$ 且高度 $ht[i] < ht[j]$ ,即 $i$ 为短板、 $j$ 为长板。
![初始状态](max_capacity_problem.assets/max_capacity_initial_state.png)
<p align="center"> 图初始状态 </p>
<p align="center"> 图 15-8 &nbsp; 初始状态 </p>
图所示,**若此时将长板 $j$ 向短板 $i$ 靠近,则容量一定变小**。这是因为在移动长板 $j$ 后:
如图 15-9 所示,**若此时将长板 $j$ 向短板 $i$ 靠近,则容量一定变小**。这是因为在移动长板 $j$ 后:
- 宽度 $j-i$ 肯定变小。
- 高度由短板决定,因此高度只可能不变( $i$ 仍为短板)或变小(移动后的 $j$ 成为短板)。
![向内移动长板后的状态](max_capacity_problem.assets/max_capacity_moving_long_board.png)
<p align="center"> 图向内移动长板后的状态 </p>
<p align="center"> 图 15-9 &nbsp; 向内移动长板后的状态 </p>
反向思考,**我们只有向内收缩短板 $i$ ,才有可能使容量变大**。因为虽然宽度一定变小,**但高度可能会变大**(移动后的短板 $i$ 可能会变长)。例如在图中,移动短板后面积变大。
反向思考,**我们只有向内收缩短板 $i$ ,才有可能使容量变大**。因为虽然宽度一定变小,**但高度可能会变大**(移动后的短板 $i$ 可能会变长)。例如在图 15-10 中,移动短板后面积变大。
![向内移动短板后的状态](max_capacity_problem.assets/max_capacity_moving_short_board.png)
<p align="center"> 图向内移动短板后的状态 </p>
<p align="center"> 图 15-10 &nbsp; 向内移动短板后的状态 </p>
由此便可推出本题的贪心策略:初始化两指针分裂容器两端,每轮向内收缩短板对应的指针,直至两指针相遇。
图展示了贪心策略的执行过程。
15-11 展示了贪心策略的执行过程。
1. 初始状态下,指针 $i$ , $j$ 分列与数组两端。
2. 计算当前状态的容量 $cap[i, j]$ ,并更新最大容量。
@@ -86,7 +86,7 @@ $$
=== "<9>"
![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png)
<p align="center"> 图最大容量问题的贪心过程 </p>
<p align="center"> 图 15-11 &nbsp; 最大容量问题的贪心过程 </p>
### 2. &nbsp; 代码实现
@@ -301,7 +301,7 @@ $$
之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。
比如在状态 $cap[i, j]$ 下,$i$ 为短板、$j$ 为长板。若贪心地将短板 $i$ 向内移动一格,会导致图所示的状态被“跳过”。**这意味着之后无法验证这些状态的容量大小**。
比如在状态 $cap[i, j]$ 下,$i$ 为短板、$j$ 为长板。若贪心地将短板 $i$ 向内移动一格,会导致图 15-12 所示的状态被“跳过”。**这意味着之后无法验证这些状态的容量大小**。
$$
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
@@ -309,7 +309,7 @@ $$
![移动短板导致被跳过的状态](max_capacity_problem.assets/max_capacity_skipped_states.png)
<p align="center"> 图移动短板导致被跳过的状态 </p>
<p align="center"> 图 15-12 &nbsp; 移动短板导致被跳过的状态 </p>
观察发现,**这些被跳过的状态实际上就是将长板 $j$ 向内移动的所有状态**。而在第二步中,我们已经证明内移长板一定会导致容量变小。也就是说,被跳过的状态都不可能是最优解,**跳过它们不会导致错过最优解**。
@@ -11,7 +11,7 @@ status: new
![最大切分乘积的问题定义](max_product_cutting_problem.assets/max_product_cutting_definition.png)
<p align="center"> 图最大切分乘积的问题定义 </p>
<p align="center"> 图 15-13 &nbsp; 最大切分乘积的问题定义 </p>
假设我们将 $n$ 切分为 $m$ 个整数因子,其中第 $i$ 个因子记为 $n_i$ ,即
@@ -39,23 +39,23 @@ n & \geq 4
\end{aligned}
$$
图所示,当 $n \geq 4$ 时,切分出一个 $2$ 后乘积会变大,**这说明大于等于 $4$ 的整数都应该被切分**。
如图 15-14 所示,当 $n \geq 4$ 时,切分出一个 $2$ 后乘积会变大,**这说明大于等于 $4$ 的整数都应该被切分**。
**贪心策略一**:如果切分方案中包含 $\geq 4$ 的因子,那么它就应该被继续切分。最终的切分方案只应出现 $1$ , $2$ , $3$ 这三种因子。
![切分导致乘积变大](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png)
<p align="center"> 图切分导致乘积变大 </p>
<p align="center"> 图 15-14 &nbsp; 切分导致乘积变大 </p>
接下来思考哪个因子是最优的。在 $1$ , $2$ , $3$ 这三个因子中,显然 $1$ 是最差的,因为 $1 \times (n-1) < n$ 恒成立,即切分出 $1$ 反而会导致乘积减小。
图所示,当 $n = 6$ 时,有 $3 \times 3 > 2 \times 2 \times 2$ 。**这意味着切分出 $3$ 比切分出 $2$ 更优**。
如图 15-15 所示,当 $n = 6$ 时,有 $3 \times 3 > 2 \times 2 \times 2$ 。**这意味着切分出 $3$ 比切分出 $2$ 更优**。
**贪心策略二**:在切分方案中,最多只应存在两个 $2$ 。因为三个 $2$ 总是可以被替换为两个 $3$ ,从而获得更大乘积。
![最优切分因子](max_product_cutting_problem.assets/max_product_cutting_greedy_infer3.png)
<p align="center"> 图最优切分因子 </p>
<p align="center"> 图 15-15 &nbsp; 最优切分因子 </p>
总结以上,可推出贪心策略:
@@ -66,7 +66,7 @@ $$
### 2. &nbsp; 代码实现
图所示,我们无须通过循环来切分整数,而可以利用向下整除运算得到 $3$ 的个数 $a$ ,用取模运算得到余数 $b$ ,此时有:
如图 15-16 所示,我们无须通过循环来切分整数,而可以利用向下整除运算得到 $3$ 的个数 $a$ ,用取模运算得到余数 $b$ ,此时有:
$$
n = 3 a + b
@@ -276,7 +276,7 @@ $$
![最大切分乘积的计算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png)
<p align="center"> 图最大切分乘积的计算方法 </p>
<p align="center"> 图 15-16 &nbsp; 最大切分乘积的计算方法 </p>
**时间复杂度取决于编程语言的幂运算的实现方法**。以 Python 为例,常用的幂计算函数有: