feat: Revised the book (#978)

* Sync recent changes to the revised Word.

* Revised the preface chapter

* Revised the introduction chapter

* Revised the computation complexity chapter

* Revised the chapter data structure

* Revised the chapter array and linked list

* Revised the chapter stack and queue

* Revised the chapter hashing

* Revised the chapter tree

* Revised the chapter heap

* Revised the chapter graph

* Revised the chapter searching

* Reivised the sorting chapter

* Revised the divide and conquer chapter

* Revised the chapter backtacking

* Revised the DP chapter

* Revised the greedy chapter

* Revised the appendix chapter

* Revised the preface chapter doubly

* Revised the figures
This commit is contained in:
Yudong Jin
2023-12-02 06:21:34 +08:00
committed by GitHub
parent b824d149cb
commit e720aa2d24
404 changed files with 1537 additions and 1558 deletions

View File

@ -2,38 +2,38 @@
!!! question
给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$、价值为 $val[i-1]$ ,和一个容量为 $cap$ 的背包。每个物品只能选择一次,**但可以选择物品的一部分,价值根据选择的重量比例计算**,问在不超过背包容量下背包中物品的最大价值。
给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$、价值为 $val[i-1]$ ,和一个容量为 $cap$ 的背包。每个物品只能选择一次,**但可以选择物品的一部分,价值根据选择的重量比例计算**,问在限定背包容量下背包中物品的最大价值。示例如下图所示。
![分数背包问题的示例数据](fractional_knapsack_problem.assets/fractional_knapsack_example.png)
分数背包和 0-1 背包整体上非常相似,状态包含当前物品 $i$ 和容量 $c$ ,目标是求不超过背包容量下的最大价值。
分数背包问题和 0-1 背包问题整体上非常相似,状态包含当前物品 $i$ 和容量 $c$ ,目标是求限定背包容量下的最大价值。
不同点在于,本题允许只选择物品的一部分。如下图所示,**我们可以对物品任意地进行切分,并按照重量比例来计算物品价值**。
不同点在于,本题允许只选择物品的一部分。如下图所示,**我们可以对物品任意地进行切分,并按照重量比例来计算相应价值**。
1. 对于物品 $i$ ,它在单位重量下的价值为 $val[i-1] / wgt[i-1]$ ,简称单位价值。
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)
### 贪心策略确定
最大化背包内物品总价值,**本质上是最大化单位重量下的物品价值**。由此便可推出下图所示的贪心策略。
最大化背包内物品总价值,**本质上是最大化单位重量下的物品价值**。由此便可推出下图所示的贪心策略。
1. 将物品按照单位价值从高到低进行排序。
2. 遍历所有物品,**每轮贪心地选择单位价值最高的物品**。
3. 若剩余背包容量不足,则使用当前物品的一部分填满背包即可
3. 若剩余背包容量不足,则使用当前物品的一部分填满背包。
![分数背包的贪心策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png)
![分数背包问题的贪心策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png)
### 代码实现
我们建立了一个物品类 `Item` ,以便将物品按照单位价值进行排序。循环进行贪心选择,当背包已满时跳出并返回解
我们建立了一个物品类 `Item` ,以便将物品按照单位价值进行排序。循环进行贪心选择,当背包已满时跳出并返回解
```src
[file]{fractional_knapsack}-[class]{}-[func]{fractional_knapsack}
```
最差情况下,需要遍历整个物品列表,**因此时间复杂度为 $O(n)$** ,其中 $n$ 为物品数量。
最差情况下,需要遍历整个物品列表,**因此时间复杂度为 $O(n)$** ,其中 $n$ 为物品数量。
由于初始化了一个 `Item` 对象列表,**因此空间复杂度为 $O(n)$** 。
@ -45,6 +45,6 @@
对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,**单位价值更大的物品总是更优选择**,这说明贪心策略是有效的。
如下图所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。
如下图所示,如果将物品重量和物品单位价值分别看作一张二维图表的横轴和纵轴,则分数背包问题可转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。
![分数背包问题的几何表示](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png)

View File

@ -1,35 +1,35 @@
# 贪心算法
「贪心算法 greedy algorithm」是一种常见的解决优化问题的算法其基本思想是在问题的每个决策阶段都选择当前看起来最优的选择即贪心地做出局部最优的决策以期获得全局最优解。贪心算法简洁且高效,在许多实际问题中有着广泛的应用。
「贪心算法 greedy algorithm」是一种常见的解决优化问题的算法其基本思想是在问题的每个决策阶段都选择当前看起来最优的选择即贪心地做出局部最优的决策以期获得全局最优解。贪心算法简洁且高效在许多实际问题中有着广泛的应用。
贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理不同
贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理不同。
- 动态规划会根据之前阶段的所有决策来考虑当前决策,并使用过去子问题的解来构建当前子问题的解。
- 贪心算法不会重新考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决。
- 贪心算法不会考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决。
我们先通过例题“零钱兑换”了解贪心算法的工作原理。这道题已经在动态规划章节中介绍过,相信你对它并不陌生。
我们先通过例题“零钱兑换”了解贪心算法的工作原理。这道题已经在“完全背包问题”章节中介绍过,相信你对它并不陌生。
!!! question
给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币数。如果无法凑出目标金额则返回 $-1$ 。
给定 $n$ 种硬币,第 $i$ 种硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币数。如果无法凑出目标金额则返回 $-1$ 。
本题的贪心策略如下图所示。给定目标金额,**我们贪心地选择不大于且最接近它的硬币**,不断循环该步骤,直至凑出目标金额为止。
本题采取的贪心策略如下图所示。给定目标金额,**我们贪心地选择不大于且最接近它的硬币**,不断循环该步骤,直至凑出目标金额为止。
![零钱兑换的贪心策略](greedy_algorithm.assets/coin_change_greedy_strategy.png)
实现代码如下所示。你可能会不由地发出感叹So Clean !贪心算法仅用十行代码就解决了零钱兑换问题
实现代码如下所示。你可能会不由地发出感叹So clean !贪心算法仅用十行代码就解决了零钱兑换问题
```src
[file]{coin_change_greedy}-[class]{}-[func]{coin_change_greedy}
```
## 贪心优点与局限性
## 贪心优点与局限性
**贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times amt)$ 提升了一个数量级。
然而,**对于某些硬币面值组合,贪心算法并不能找到最优解**。下图给出了两个示例。
- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:在该硬币组合下,给定任意 $amt$ ,贪心算法都可以找最优解。
- **正例 $coins = [1, 5, 10, 20, 50, 100]$**:在该硬币组合下,给定任意 $amt$ ,贪心算法都可以找最优解。
- **反例 $coins = [1, 20, 50]$**:假设 $amt = 60$ ,贪心算法只能找到 $50 + 1 \times 10$ 的兑换组合,共计 $11$ 枚硬币,但动态规划可以找到最优解 $20 + 20 + 20$ ,仅需 $3$ 枚硬币。
- **反例 $coins = [1, 49, 50]$**:假设 $amt = 98$ ,贪心算法只能找到 $50 + 1 \times 48$ 的兑换组合,共计 $49$ 枚硬币,但动态规划可以找到最优解 $49 + 49$ ,仅需 $2$ 枚硬币。
@ -37,10 +37,10 @@
也就是说,对于零钱兑换问题,贪心算法无法保证找到全局最优解,并且有可能找到非常差的解。它更适合用动态规划解决。
一般情况下,贪心算法适用于以下两类问题
一般情况下,贪心算法适用情况分以下两种
1. **可以保证找到最优解**:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效。
2. **可以找到近似最优解**:贪心算法在这种情况下也是可用的。对于很多复杂问题来说,寻找全局最优解非常困难,能以较高效率找到次优解也是非常不错的。
2. **可以找到近似最优解**:贪心算法在这种情况下也是可用的。对于很多复杂问题来说,寻找全局最优解非常困难,能以较高效率找到次优解也是非常不错的。
## 贪心算法特性
@ -51,15 +51,15 @@
- **贪心选择性质**:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证得到最优解。
- **最优子结构**:原问题的最优解包含子问题的最优解。
最优子结构已经在动态规划章节中介绍过,不再赘述。值得注意的是,一些问题的最优子结构并不明显,但仍然可使用贪心算法解决。
最优子结构已经在动态规划章节中介绍过,这里不再赘述。值得注意的是,一些问题的最优子结构并不明显,但仍然可使用贪心算法解决。
我们主要探究贪心选择性质的判断方法。虽然它的描述看上去比较简单,**但实际上对于许多问题,证明贪心选择性质不是一件易事**。
我们主要探究贪心选择性质的判断方法。虽然它的描述看上去比较简单,**但实际上对于许多问题,证明贪心选择性质并非易事**。
例如零钱兑换问题,我们虽然能够容易地举出反例,对贪心选择性质进行证伪,但证实的难度较大。如果问:**满足什么条件的硬币组合可以使用贪心算法求解**?我们往往只能凭借直觉或举例子来给出一个模棱两可的答案,而难以给出严谨的数学证明。
!!! quote
有一篇论文给出了一个 $O(n^3)$ 时间复杂度的算法,用于判断一个硬币组合是否可以使用贪心算法找出任金额的最优解。
有一篇论文给出了一个 $O(n^3)$ 时间复杂度的算法,用于判断一个硬币组合能否使用贪心算法找出任金额的最优解。
Pearson, David. A polynomial-time algorithm for the change-making problem. Operations Research Letters 33.3 (2005): 231-234.
@ -68,17 +68,17 @@
贪心问题的解决流程大体可分为以下三步。
1. **问题分析**:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。这一步在回溯和动态规划中都有涉及。
2. **确定贪心策略**:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终解决整个问题。
3. **正确性证明**:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要使用到数学证明,例如归纳法或反证法等。
2. **确定贪心策略**:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终解决整个问题。
3. **正确性证明**:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要用到数学证明,例如归纳法或反证法等。
确定贪心策略是求解问题的核心步骤,但实施起来可能并不容易,主要包含以下原因。
确定贪心策略是求解问题的核心步骤,但实施起来可能并不容易,主要以下原因。
- **不同问题的贪心策略的差异较大**。对于许多问题来说,贪心策略比较浅显,我们通过一些大概的思考与尝试就能得出。而对于一些复杂问题,贪心策略可能非常隐蔽,这种情况就非常考验个人的解题经验与算法能力了。
- **某些贪心策略具有较强的迷惑性**。当我们满怀信心设计好贪心策略,写出解题代码并提交运行,很可能发现部分测试样例无法通过。这是因为设计的贪心策略只是“部分正确”的,上文介绍的零钱兑换就是个典型案例。
- **不同问题的贪心策略的差异较大**。对于许多问题来说,贪心策略比较浅显,我们通过一些大概的思考与尝试就能得出。而对于一些复杂问题,贪心策略可能非常隐蔽,这种情况就非常考验个人的解题经验与算法能力了。
- **某些贪心策略具有较强的迷惑性**。当我们满怀信心设计好贪心策略,写出解题代码并提交运行,很可能发现部分测试样例无法通过。这是因为设计的贪心策略只是“部分正确”的,上文介绍的零钱兑换就是个典型案例。
为了保证正确性,我们应该对贪心策略进行严谨的数学证明,**通常需要用到反证法或数学归纳法**。
然而,正确性证明也很可能不是一件易事。如若没有头绪,我们通常会选择面向测试用例进行 Debug ,一步步修改与验证贪心策略。
然而,正确性证明也很可能不是一件易事。如若没有头绪,我们通常会选择面向测试用例进行代码调试,一步步修改与验证贪心策略。
## 贪心典型例题
@ -88,5 +88,5 @@
- **区间调度问题**:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
- **分数背包问题**:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解。
- **股票买卖问题**:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
- **霍夫曼编码**:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最的两个节点合并,最后得到的霍夫曼树的带权路径长度(编码长度)最小。
- **霍夫曼编码**:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最的两个节点合并,最后得到的霍夫曼树的带权路径长度(编码长度)最小。
- **Dijkstra 算法**:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。

View File

@ -8,6 +8,6 @@
!!! abstract
向日葵朝着太阳转动,时刻都在追求自身成长的最大可能。
向日葵朝着太阳转动,时刻追求自身成长的最大可能。
贪心策略在一轮轮的简单选择中,逐步导向最佳答案。
贪心策略在一轮轮的简单选择中,逐步导向最佳答案。

View File

@ -2,23 +2,23 @@
!!! question
输入一个数组 $ht$ 数组中的每个元素代表一个垂直隔板的高度。数组中的任意两个隔板,以及它们之间的空间可以组成一个容器。
输入一个数组 $ht$ 中的每个元素代表一个垂直隔板的高度。数组中的任意两个隔板,以及它们之间的空间可以组成一个容器。
容器的容量等于高度和宽度的乘积(面积),其中高度由较短的隔板决定,宽度是两个隔板的数组索引之差。
容器的容量等于高度和宽度的乘积(面积),其中高度由较短的隔板决定,宽度是两个隔板的数组索引之差。
请在数组中选择两个隔板,使得组成的容器的容量最大,返回最大容量。
请在数组中选择两个隔板,使得组成的容器的容量最大,返回最大容量。示例如下图所示。
![最大容量问题的示例数据](max_capacity_problem.assets/max_capacity_example.png)
容器由任意两个隔板围成,**因此本题的状态为两个隔板的索引,记为 $[i, j]$** 。
根据题意,容量等于高度乘以宽度,其中高度由短板决定,宽度是两隔板的索引之差。设容量为 $cap[i, j]$ ,则可得计算公式:
根据题意,容量等于高度乘以宽度,其中高度由短板决定,宽度是两隔板的数组索引之差。设容量为 $cap[i, j]$ ,则可得计算公式:
$$
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
$$
设数组长度为 $n$ ,两个隔板的组合数量(状态总数)为 $C_n^2 = \frac{n(n - 1)}{2}$ 个。最直接地,**我们可以穷举所有状态**,从而求得最大容量,时间复杂度为 $O(n^2)$ 。
设数组长度为 $n$ ,两个隔板的组合数量(状态总数)为 $C_n^2 = \frac{n(n - 1)}{2}$ 个。最直接地,**我们可以穷举所有状态**,从而求得最大容量,时间复杂度为 $O(n^2)$ 。
### 贪心策略确定
@ -36,14 +36,14 @@ $$
![向内移动短板后的状态](max_capacity_problem.assets/max_capacity_moving_short_board.png)
由此便可推出本题的贪心策略初始化两指针分容器两端每轮向内收缩短板对应的指针直至两指针相遇
由此便可推出本题的贪心策略初始化两指针分容器两端每轮向内收缩短板对应的指针直至两指针相遇
下图展示了贪心策略的执行过程
1. 初始状态下指针 $i$ $j$ 分列与数组两端
2. 计算当前状态的容量 $cap[i, j]$ 并更新最大容量
3. 比较板 $i$ $j$ 的高度并将短板向内移动一格
4. 循环执行第 `2.` `3.` 直至 $i$ $j$ 相遇时结束
4. 循环执行第 `2.` 步和第 `3.` 直至 $i$ $j$ 相遇时结束
=== "<1>"
![最大容量问题的贪心过程](max_capacity_problem.assets/max_capacity_greedy_step1.png)
@ -76,7 +76,7 @@ $$
代码循环最多 $n$ 轮,**因此时间复杂度为 $O(n)$** 。
变量 $i$、$j$、$res$ 使用常数大小额外空间,**因此空间复杂度为 $O(1)$** 。
变量 $i$、$j$、$res$ 使用常数大小额外空间,**因此空间复杂度为 $O(1)$** 。
```src
[file]{max_capacity}-[class]{}-[func]{max_capacity}
@ -94,6 +94,6 @@ $$
![移动短板导致被跳过的状态](max_capacity_problem.assets/max_capacity_skipped_states.png)
观察发现,**这些被跳过的状态实际上就是将长板 $j$ 向内移动的所有状态**。而在第二步中,我们已经证明内移长板一定会导致容量变小。也就是说,被跳过的状态都不可能是最优解,**跳过它们不会导致错过最优解**。
观察发现,**这些被跳过的状态实际上就是将长板 $j$ 向内移动的所有状态**。前面我们已经证明内移长板一定会导致容量变小。也就是说,被跳过的状态都不可能是最优解,**跳过它们不会导致错过最优解**。
以上分析说明,**移动短板的操作是“安全”的**,贪心策略是有效的。
以上分析说明,移动短板的操作是“安全”的,贪心策略是有效的。

View File

@ -2,7 +2,7 @@
!!! question
给定一个正整数 $n$ ,将其切分为至少两个正整数的和,求切分后所有整数的乘积最大是多少。
给定一个正整数 $n$ ,将其切分为至少两个正整数的和,求切分后所有整数的乘积最大是多少,如下图所示
![最大切分乘积的问题定义](max_product_cutting_problem.assets/max_product_cutting_definition.png)
@ -12,7 +12,7 @@ $$
n = \sum_{i=1}^{m}n_i
$$
本题目标是求得所有整数因子的最大乘积,即
本题目标是求得所有整数因子的最大乘积,即
$$
\max(\prod_{i=1}^{m}n_i)
@ -42,11 +42,11 @@ $$
如下图所示 $n = 6$ $3 \times 3 > 2 \times 2 \times 2$ 。**这意味着切分出 $3$ 比切分出 $2$ 更优**。
**贪心策略二**:在切分方案中,最多只应存在两个 $2$ 。因为三个 $2$ 总是可以替换为两个 $3$ ,从而获得更大乘积。
**贪心策略二**:在切分方案中,最多只应存在两个 $2$ 。因为三个 $2$ 总是可以替换为两个 $3$ ,从而获得更大乘积。
![最优切分因子](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png)
总结以上,可推出以下贪心策略。
综上所述,可推出以下贪心策略。
1. 输入整数 $n$ ,从其不断地切分出因子 $3$ ,直至余数为 $0$、$1$、$2$ 。
2. 当余数为 $0$ 时,代表 $n$ 是 $3$ 的倍数,因此不做任何处理。
@ -81,5 +81,5 @@ $$
使用反证法,只分析 $n \geq 3$ 的情况。
1. **所有因子 $\leq 3$** :假设最优切分方案中存在 $\geq 4$ 的因子 $x$ ,那么一定可以将其继续划分为 $2(x-2)$ ,从而获得更大的乘积。这与假设矛盾。
2. **切分方案不包含 $1$** :假设最优切分方案中存在一个因子 $1$ ,那么它一定可以合并入另外一个因子中,以获更大乘积。这与假设矛盾。
2. **切分方案不包含 $1$** :假设最优切分方案中存在一个因子 $1$ ,那么它一定可以合并入另外一个因子中,以获更大乘积。这与假设矛盾。
3. **切分方案最多包含两个 $2$** :假设最优切分方案中包含三个 $2$ ,那么一定可以替换为两个 $3$ ,乘积更大。这与假设矛盾。

View File

@ -1,12 +1,12 @@
# 小结
- 贪心算法通常用于解决最优化问题,其原理是在每个决策阶段都做出局部最优的决策,以期获得全局最优解。
- 贪心算法通常用于解决最优化问题,其原理是在每个决策阶段都做出局部最优的决策,以期获得全局最优解。
- 贪心算法会迭代地做出一个又一个的贪心选择,每轮都将问题转化成一个规模更小的子问题,直到问题被解决。
- 贪心算法不仅实现简单,还具有很高的解题效率。相比于动态规划,贪心算法的时间复杂度通常更低。
- 在零钱兑换问题中,对于某些硬币组合,贪心算法可以保证找到最优解;对于另外一些硬币组合则不然,贪心算法可能找到很差的解。
- 适合用贪心算法求解的问题具有两大性质:贪心选择性质和最优子结构。贪心选择性质代表贪心策略的有效性。
- 对于某些复杂问题,贪心选择性质的证明并不简单。相对来说,证伪更加容易,例如零钱兑换问题。
- 求解贪心问题主要分为三步:问题分析、贪心策略确定、正确性证明。其中,贪心策略确定是核心步骤,正确性证明往往是难点。
- 求解贪心问题主要分为三步:问题分析、确定贪心策略、正确性证明。其中,确定贪心策略是核心步骤,正确性证明往往是难点。
- 分数背包问题在 0-1 背包的基础上,允许选择物品的一部分,因此可使用贪心算法求解。贪心策略的正确性可以使用反证法来证明。
- 最大容量问题可使用穷举法求解,时间复杂度为 $O(n^2)$ 。通过设计贪心策略,每轮向内移动短板,可将时间复杂度优化至 $O(n)$ 。
- 在最大切分乘积问题中,我们先后推理出两个贪心策略:$\geq 4$ 的整数都应该继续切分、最优切分因子为 $3$ 。代码中包含幂运算,时间复杂度取决于幂运算实现方法,通常为 $O(1)$ 或 $O(\log n)$ 。