diff --git a/codes/cpp/CMakeLists.txt b/codes/cpp/CMakeLists.txt index 9e3dffc78..1e80bc4d7 100644 --- a/codes/cpp/CMakeLists.txt +++ b/codes/cpp/CMakeLists.txt @@ -17,3 +17,4 @@ add_subdirectory(chapter_sorting) add_subdirectory(chapter_divide_and_conquer) add_subdirectory(chapter_backtracking) add_subdirectory(chapter_dynamic_programming) +add_subdirectory(chapter_greedy) diff --git a/codes/cpp/chapter_greedy/CMakeLists.txt b/codes/cpp/chapter_greedy/CMakeLists.txt new file mode 100644 index 000000000..96ee4d218 --- /dev/null +++ b/codes/cpp/chapter_greedy/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(coin_change_greedy coin_change_greedy.cpp) +add_executable(fractional_knapsack fractional_knapsack.cpp) \ No newline at end of file diff --git a/codes/cpp/chapter_greedy/coin_change_greedy.cpp b/codes/cpp/chapter_greedy/coin_change_greedy.cpp new file mode 100644 index 000000000..7e1283324 --- /dev/null +++ b/codes/cpp/chapter_greedy/coin_change_greedy.cpp @@ -0,0 +1,60 @@ +/** + * File: coin_change_greedy.cpp + * Created Time: 2023-07-20 + * Author: Krahets (krahets@163.com) + */ + +#include "../utils/common.hpp" + +/* 零钱兑换:贪心 */ +int coinChangeGreedy(vector &coins, int amt) { + // 假设 coins 列表有序 + int i = coins.size() - 1; + int count = 0; + // 循环进行贪心选择,直到无剩余金额 + while (amt > 0) { + // 找到小于且最接近剩余金额的硬币 + while (coins[i] > amt) { + i--; + } + // 选择 coins[i] + amt -= coins[i]; + count++; + } + // 若未找到可行方案,则返回 -1 + return amt == 0 ? count : -1; +} + +/* Driver Code */ +int main() { + // 贪心:能够保证找到全局最优解 + vector coins = {1, 5, 10, 20, 50, 100}; + int amt = 186; + int res = coinChangeGreedy(coins, amt); + cout << "\ncoins = "; + printVector(coins); + cout << "amt = " << amt << endl; + cout << "凑到 " << amt << " 所需的最少硬币数量为 " << res << endl; + + // 贪心:无法保证找到全局最优解 + coins = {1, 20, 50}; + amt = 60; + res = coinChangeGreedy(coins, amt); + cout << "\ncoins = ["; + printVector(coins); + cout << "amt = " << amt << endl; + cout << "凑到 " << amt << " 所需的最少硬币数量为 " << res << endl; + cout << "实际上需要的最少数量为 3 ,即 20 + 20 + 20" << endl; + + // 贪心:无法保证找到全局最优解 + coins = {1, 49, 50}; + amt = 98; + res = coinChangeGreedy(coins, amt); + cout << "\ncoins = ["; + printVector(coins); + cout << "amt = " << amt << endl; + cout << "凑到 " << amt << " 所需的最少硬币数量为 " << res << endl; + cout << "实际上需要的最少数量为 2 ,即 49 + 49" << endl; + + return 0; +} diff --git a/codes/cpp/chapter_greedy/fractional_knapsack.cpp b/codes/cpp/chapter_greedy/fractional_knapsack.cpp new file mode 100644 index 000000000..f0335d1d6 --- /dev/null +++ b/codes/cpp/chapter_greedy/fractional_knapsack.cpp @@ -0,0 +1,56 @@ +/** + * File: fractional_knapsack.cpp + * Created Time: 2023-07-20 + * Author: Krahets (krahets@163.com) + */ + +#include "../utils/common.hpp" + +/* 物品 */ +class Item { + public: + int w; // 物品重量 + int v; // 物品价值 + + Item(int w, int v) : w(w), v(v) { + } +}; + +/* 分数背包:贪心 */ +double fractionalKnapsack(vector &wgt, vector &val, int cap) { + // 创建物品列表,包含两个属性:重量、价值 + vector items; + for (int i = 0; i < wgt.size(); i++) { + items.push_back(Item(wgt[i], val[i])); + } + // 按照单位价值 item.v / item.w 从高到低进行排序 + sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; }); + // 循环贪心选择 + double res = 0; + for (auto &item : items) { + if (item.w <= cap) { + // 若剩余容量充足,则将当前物品整个装进背包 + res += item.v; + cap -= item.w; + } else { + // 若剩余容量不足,则将当前物品的一部分装进背包 + res += (double)item.v / item.w * cap; + // 已无剩余容量,因此跳出循环 + break; + } + } + return res; +} + +/* Driver Code */ +int main() { + vector wgt = {10, 20, 30, 40, 50}; + vector val = {50, 120, 150, 210, 240}; + int cap = 50; + + // 贪心算法 + double res = fractionalKnapsack(wgt, val, cap); + cout << "不超过背包容量的最大物品价值为 " << res << endl; + + return 0; +} diff --git a/codes/cpp/chapter_hashing/CMakeLists.txt b/codes/cpp/chapter_hashing/CMakeLists.txt index 6b4bf536a..6b583ef55 100644 --- a/codes/cpp/chapter_hashing/CMakeLists.txt +++ b/codes/cpp/chapter_hashing/CMakeLists.txt @@ -3,4 +3,4 @@ add_executable(array_hash_map_test array_hash_map_test.cpp) add_executable(hash_map_chaining hash_map_chaining.cpp) add_executable(hash_map_open_addressing hash_map_open_addressing.cpp) add_executable(simple_hash simple_hash.cpp) -add_executable(built_in_hash built_in_hash.cpp) +add_executable(built_in_hash built_in_hash.cpp) \ No newline at end of file diff --git a/codes/java/chapter_greedy/coin_change_greedy.java b/codes/java/chapter_greedy/coin_change_greedy.java new file mode 100644 index 000000000..3b2ca3a96 --- /dev/null +++ b/codes/java/chapter_greedy/coin_change_greedy.java @@ -0,0 +1,55 @@ +/** + * File: coin_change_greedy.java + * Created Time: 2023-07-20 + * Author: Krahets (krahets@163.com) + */ + +package chapter_greedy; + +import java.util.Arrays; + +public class coin_change_greedy { + /* 零钱兑换:贪心 */ + static int coinChangeGreedy(int[] coins, int amt) { + // 假设 coins 列表有序 + int i = coins.length - 1; + int count = 0; + // 循环进行贪心选择,直到无剩余金额 + while (amt > 0) { + // 找到小于且最接近剩余金额的硬币 + while (coins[i] > amt) { + i--; + } + // 选择 coins[i] + amt -= coins[i]; + count++; + } + // 若未找到可行方案,则返回 -1 + return amt == 0 ? count : -1; + } + + public static void main(String[] args) { + // 贪心:能够保证找到全局最优解 + int[] coins = { 1, 5, 10, 20, 50, 100 }; + int amt = 186; + int res = coinChangeGreedy(coins, amt); + System.out.println("\ncoins = " + Arrays.toString(coins) + ", amt = " + amt); + System.out.println("凑到 " + amt + " 所需的最少硬币数量为 " + res); + + // 贪心:无法保证找到全局最优解 + coins = new int[] { 1, 20, 50 }; + amt = 60; + res = coinChangeGreedy(coins, amt); + System.out.println("\ncoins = " + Arrays.toString(coins) + ", amt = " + amt); + System.out.println("凑到 " + amt + " 所需的最少硬币数量为 " + res); + System.out.println("实际上需要的最少数量为 3 ,即 20 + 20 + 20"); + + // 贪心:无法保证找到全局最优解 + coins = new int[] { 1, 49, 50 }; + amt = 98; + res = coinChangeGreedy(coins, amt); + System.out.println("\ncoins = " + Arrays.toString(coins) + ", amt = " + amt); + System.out.println("凑到 " + amt + " 所需的最少硬币数量为 " + res); + System.out.println("实际上需要的最少数量为 2 ,即 49 + 49"); + } +} diff --git a/codes/java/chapter_greedy/fractional_knapsack.java b/codes/java/chapter_greedy/fractional_knapsack.java new file mode 100644 index 000000000..0ecb407b3 --- /dev/null +++ b/codes/java/chapter_greedy/fractional_knapsack.java @@ -0,0 +1,59 @@ +/** + * File: fractional_knapsack.java + * Created Time: 2023-07-20 + * Author: Krahets (krahets@163.com) + */ + +package chapter_greedy; + +import java.util.Arrays; +import java.util.Comparator; + +/* 物品 */ +class Item { + int w; // 物品重量 + int v; // 物品价值 + + public Item(int w, int v) { + this.w = w; + this.v = v; + } +} + +public class fractional_knapsack { + /* 分数背包:贪心 */ + static double fractionalKnapsack(int[] wgt, int[] val, int cap) { + // 创建物品列表,包含两个属性:重量、价值 + Item[] items = new Item[wgt.length]; + for (int i = 0; i < wgt.length; i++) { + items[i] = new Item(wgt[i], val[i]); + } + // 按照单位价值 item.v / item.w 从高到低进行排序 + Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w))); + // 循环贪心选择 + double res = 0; + for (Item item : items) { + if (item.w <= cap) { + // 若剩余容量充足,则将当前物品整个装进背包 + res += item.v; + cap -= item.w; + } else { + // 若剩余容量不足,则将当前物品的一部分装进背包 + res += (double) item.v / item.w * cap; + // 已无剩余容量,因此跳出循环 + break; + } + } + return res; + } + + public static void main(String[] args) { + int[] wgt = { 10, 20, 30, 40, 50 }; + int[] val = { 50, 120, 150, 210, 240 }; + int cap = 50; + + // 贪心算法 + double res = fractionalKnapsack(wgt, val, cap); + System.out.println("不超过背包容量的最大物品价值为 " + res); + } +} diff --git a/codes/python/chapter_greedy/coin_change_greedy.py b/codes/python/chapter_greedy/coin_change_greedy.py new file mode 100644 index 000000000..c2e4be817 --- /dev/null +++ b/codes/python/chapter_greedy/coin_change_greedy.py @@ -0,0 +1,48 @@ +""" +File: coin_change_greedy.py +Created Time: 2023-07-18 +Author: Krahets (krahets@163.com) +""" + + +def coin_change_greedy(coins: list[int], amt: int) -> int: + """零钱兑换:贪心""" + # 假设 coins 列表有序 + i = len(coins) - 1 + count = 0 + # 循环进行贪心选择,直到无剩余金额 + while amt > 0: + # 找到小于且最接近剩余金额的硬币 + while coins[i] > amt: + i -= 1 + # 选择 coins[i] + amt -= coins[i] + count += 1 + # 若未找到可行方案,则返回 -1 + return count if amt == 0 else -1 + + +"""Driver Code""" +if __name__ == "__main__": + # 贪心:能够保证找到全局最优解 + coins = [1, 5, 10, 20, 50, 100] + amt = 186 + res = coin_change_greedy(coins, amt) + print(f"\ncoins = {coins}, amt = {amt}") + print(f"凑到 {amt} 所需的最少硬币数量为 {res}") + + # 贪心:无法保证找到全局最优解 + coins = [1, 20, 50] + amt = 60 + res = coin_change_greedy(coins, amt) + print(f"\ncoins = {coins}, amt = {amt}") + print(f"凑到 {amt} 所需的最少硬币数量为 {res}") + print(f"实际上需要的最少数量为 3 ,即 20 + 20 + 20") + + # 贪心:无法保证找到全局最优解 + coins = [1, 49, 50] + amt = 98 + res = coin_change_greedy(coins, amt) + print(f"\ncoins = {coins}, amt = {amt}") + print(f"凑到 {amt} 所需的最少硬币数量为 {res}") + print(f"实际上需要的最少数量为 2 ,即 49 + 49") diff --git a/codes/python/chapter_greedy/fractional_knapsack.py b/codes/python/chapter_greedy/fractional_knapsack.py new file mode 100644 index 000000000..0191bdbdd --- /dev/null +++ b/codes/python/chapter_greedy/fractional_knapsack.py @@ -0,0 +1,46 @@ +""" +File: fractional_knapsack.py +Created Time: 2023-07-19 +Author: Krahets (krahets@163.com) +""" + + +class Item: + """物品""" + + def __init__(self, w: int, v: int): + self.w = w # 物品重量 + self.v = v # 物品价值 + + +def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int: + """分数背包:贪心""" + # 创建物品列表,包含两个属性:重量、价值 + items = [Item(w, v) for w, v in zip(wgt, val)] + # 按照单位价值 item.v / item.w 从高到低进行排序 + items.sort(key=lambda item: item.v / item.w, reverse=True) + # 循环贪心选择 + res = 0 + for item in items: + if item.w <= cap: + # 若剩余容量充足,则将当前物品整个装进背包 + res += item.v + cap -= item.w + else: + # 若剩余容量不足,则将当前物品的一部分装进背包 + res += (item.v / item.w) * cap + # 已无剩余容量,因此跳出循环 + break + return res + + +"""Driver Code""" +if __name__ == "__main__": + wgt = [10, 20, 30, 40, 50] + val = [50, 120, 150, 210, 240] + cap = 50 + n = len(wgt) + + # 贪心算法 + res = fractional_knapsack(wgt, val, cap) + print(f"不超过背包容量的最大物品价值为 {res}") diff --git a/docs/assets/covers/chapter_greedy.jpg b/docs/assets/covers/chapter_greedy.jpg new file mode 100644 index 000000000..543940c9f Binary files /dev/null and b/docs/assets/covers/chapter_greedy.jpg differ diff --git a/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png new file mode 100644 index 000000000..15b8e53ca Binary files /dev/null and b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png differ diff --git a/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_example.png b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_example.png new file mode 100644 index 000000000..74ce89b48 Binary files /dev/null and b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_example.png differ diff --git a/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png new file mode 100644 index 000000000..f0207af65 Binary files /dev/null and b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png differ diff --git a/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png new file mode 100644 index 000000000..b56e3b4f4 Binary files /dev/null and b/docs/chapter_greedy/fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png differ diff --git a/docs/chapter_greedy/fractional_knapsack_problem.md b/docs/chapter_greedy/fractional_knapsack_problem.md new file mode 100644 index 000000000..70ce3dbb0 --- /dev/null +++ b/docs/chapter_greedy/fractional_knapsack_problem.md @@ -0,0 +1,136 @@ +# 分数背包问题 + +分数背包是 0-1 背包问题的一个变种问题。 + +!!! question + + 给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$ 、价值为 $val[i-1]$ ,现在有个容量为 $cap$ 的背包,每个物品只能选择一次,**但可以选择物品的一部分,价值根据选择的重量比例计算**,问在不超过背包容量下背包中物品的最大价值。 + +![分数背包问题的示例数据](fractional_knapsack_problem.assets/fractional_knapsack_example.png) + +**第一步:问题分析** + +本题和 0-1 背包整体上非常相似,状态包含当前物品 $i$ 和容量 $c$ ,目标是求不超过背包容量下的最大价值。 + +不同点在于,本题允许只选择物品的一部分,我们可以对物品任意地进行切分,并按照重量比例来计算物品价值,因此有: + +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. 若剩余背包容量不足,则使用当前物品的一部分填满背包即可。 + +![分数背包的贪心策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png) + +**第三步:正确性证明** + +采用反证法。假设物品 $x$ 是单位价值最高的物品,使用某算法求得最大价值为 $res$ ,但该解中不包含物品 $x$ 。 + +现在从背包中拿出单位重量的任意物品,并替换为单位重量的物品 $x$ 。由于物品 $x$ 的单位价值最高,因此替换后的总价值一定大于 $res$ 。**这与 $res$ 是最优解矛盾,说明最优解中必须包含物品 $x$ 。** + +对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,**单位价值更大的物品总是更优选择**,这说明贪心策略是有效的。 + +**实现代码** + +我们构建了一个物品类 `Item` ,以便将物品按照单位价值进行排序。在循环贪心选择中,分为放入整个物品或放入部分物品两种情况。当背包已满时,则跳出循环并返回解。 + +=== "Java" + + ```java title="fractional_knapsack.java" + [class]{Item}-[func]{} + + [class]{fractional_knapsack}-[func]{fractionalKnapsack} + ``` + +=== "C++" + + ```cpp title="fractional_knapsack.cpp" + [class]{Item}-[func]{} + + [class]{}-[func]{fractionalKnapsack} + ``` + +=== "Python" + + ```python title="fractional_knapsack.py" + [class]{Item}-[func]{} + + [class]{}-[func]{fractional_knapsack} + ``` + +=== "Go" + + ```go title="fractional_knapsack.go" + [class]{}-[func]{II} + + [class]{}-[func]{fractionalKnapsack} + ``` + +=== "JavaScript" + + ```javascript title="fractional_knapsack.js" + [class]{Item}-[func]{} + + [class]{}-[func]{fractionalKnapsack} + ``` + +=== "TypeScript" + + ```typescript title="fractional_knapsack.ts" + [class]{Item}-[func]{} + + [class]{}-[func]{fractionalKnapsack} + ``` + +=== "C" + + ```c title="fractional_knapsack.c" + [class]{Item}-[func]{} + + [class]{}-[func]{fractionalKnapsack} + ``` + +=== "C#" + + ```csharp title="fractional_knapsack.cs" + [class]{Item}-[func]{} + + [class]{fractional_knapsack}-[func]{fractionalKnapsack} + ``` + +=== "Swift" + + ```swift title="fractional_knapsack.swift" + [class]{Item}-[func]{} + + [class]{}-[func]{fractionalKnapsack} + ``` + +=== "Zig" + + ```zig title="fractional_knapsack.zig" + [class]{Item}-[func]{} + + [class]{}-[func]{fractionalKnapsack} + ``` + +=== "Dart" + + ```dart title="fractional_knapsack.dart" + [class]{Item}-[func]{} + + [class]{}-[func]{fractionalKnapsack} + ``` + +如下图所示,如果将一个 2D 图表的横轴和纵轴分别看作物品重量和物品单位价值,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度清晰地看到贪心策略的有效性。 + +![分数背包问题的几何表示](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png) + +最差情况下,需要遍历整个物品列表,**因此时间复杂度为 $O(n)$** ,其中 $n$ 为物品数量。由于初始化了一个 `Item` 对象列表,**因此空间复杂度为 $O(n)$** 。 diff --git a/docs/chapter_greedy/greedy_algorithm.assets/coin_change_greedy_strategy.png b/docs/chapter_greedy/greedy_algorithm.assets/coin_change_greedy_strategy.png new file mode 100644 index 000000000..a351f0148 Binary files /dev/null and b/docs/chapter_greedy/greedy_algorithm.assets/coin_change_greedy_strategy.png differ diff --git a/docs/chapter_greedy/greedy_algorithm.assets/coin_change_greedy_vs_dp.png b/docs/chapter_greedy/greedy_algorithm.assets/coin_change_greedy_vs_dp.png new file mode 100644 index 000000000..ea6fb6cdc Binary files /dev/null and b/docs/chapter_greedy/greedy_algorithm.assets/coin_change_greedy_vs_dp.png differ diff --git a/docs/chapter_greedy/greedy_algorithm.md b/docs/chapter_greedy/greedy_algorithm.md new file mode 100644 index 000000000..901b0f26a --- /dev/null +++ b/docs/chapter_greedy/greedy_algorithm.md @@ -0,0 +1,152 @@ +# 贪心算法 + +贪心算法是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即“贪心”地做出局部最优的决策,以期望获得全局最优解。贪心算法因其简洁、高效的特性,在许多实际问题中都有着广泛的应用。 + +贪心算法和动态规划都是解决优化问题的常用策略,它们有一些相似之处,比如都依赖最优子结构性质。两者的不同点在于: + +- **动态规划会根据之前阶段的所有决策来考虑当前决策**,并使用过去子问题的解来构建当前子问题的解。 +- **贪心算法从不重新考虑过去的决策**,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决。 + +我们先通过例题“零钱兑换”来初步了解贪心算法的工作原理。这道题已经在动态规划章节中介绍过,相信你对它并不陌生。 + +!!! question + + 给定 $n$ 种硬币,第 $i$ 个硬币的面值为 $coins[i - 1]$ ,目标金额为 $amt$ ,**每种硬币可以重复选取**,问能够凑出目标金额的最少硬币个数。如果无法凑出目标金额则返回 $-1$ 。 + +贪心算法会迭代地做出一个又一个的贪心选择,每轮都将问题转化成一个规模更小的子问题,直到问题被解决。 + +这道题的贪心策略在生活中很常见:给定目标金额,**我们贪心地选择不大于且最接近它的硬币**,不断循环该步骤,直至凑出目标金额为止。 + +![零钱兑换的贪心策略](greedy_algorithm.assets/coin_change_greedy_strategy.png) + +实现代码如下所示。你可能会不由地发出感叹:So Clean !因为贪心算法仅用十行代码就解决了零钱兑换问题。 + +=== "Java" + + ```java title="coin_change_greedy.java" + [class]{coin_change_greedy}-[func]{coinChangeGreedy} + ``` + +=== "C++" + + ```cpp title="coin_change_greedy.cpp" + [class]{}-[func]{coinChangeGreedy} + ``` + +=== "Python" + + ```python title="coin_change_greedy.py" + [class]{}-[func]{coin_change_greedy} + ``` + +=== "Go" + + ```go title="coin_change_greedy.go" + [class]{}-[func]{coinChangeGreedy} + ``` + +=== "JavaScript" + + ```javascript title="coin_change_greedy.js" + [class]{}-[func]{coinChangeGreedy} + ``` + +=== "TypeScript" + + ```typescript title="coin_change_greedy.ts" + [class]{}-[func]{coinChangeGreedy} + ``` + +=== "C" + + ```c title="coin_change_greedy.c" + [class]{}-[func]{coinChangeGreedy} + ``` + +=== "C#" + + ```csharp title="coin_change_greedy.cs" + [class]{coin_change_greedy}-[func]{coinChangeGreedy} + ``` + +=== "Swift" + + ```swift title="coin_change_greedy.swift" + [class]{}-[func]{coinChangeGreedy} + ``` + +=== "Zig" + + ```zig title="coin_change_greedy.zig" + [class]{}-[func]{coinChangeGreedy} + ``` + +=== "Dart" + + ```dart title="coin_change_greedy.dart" + [class]{}-[func]{coinChangeGreedy} + ``` + +## 贪心优点与局限性 + +**贪心算法不仅操作直接、实现简单,而且通常效率也很高**。在以上代码中,记硬币最小面值为 $\min(coins)$ ,则贪心选择最多循环 $amt / \min(coins)$ 次,时间复杂度为 $O(amt / \min(coins))$ 。这比动态规划解法的时间复杂度 $O(n \times 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$ 的兑换组合,共计 $48$ 枚硬币,但动态规划可以找到最优解 $49 + 49$ ,仅需 $2$ 枚硬币。 + +![贪心无法找出最优解的示例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png) + +也就是说,对于零钱兑换问题,贪心算法无法保证找到全局最优解,并且有可能找到非常差的解,因此该问题更适合用动态规划解决。 + +一般情况下,贪心算法适用于以下两种问题: + +1. **可以保证找到最优解**:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效。 +2. **可以找到近似最优解**:此时贪心算法也是可用的。因为对于很多复杂问题来说,寻找最优解是非常困难的,能以较高效率找到次优解也是非常不错的。 + +## 贪心算法特性 + +那么问题来了,什么样的问题适合用贪心算法求解呢?或者说,贪心算法在什么情况下可以保证找到最优解? + +相较于动态规划,贪心算法的使用条件更加苛刻,其主要关心问题的两个性质: + +- **贪心选择性质**:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证得到最优解。 +- **最优子结构**:原问题的最优解包含子问题的最优解。值得注意的是,一些问题的最优子结构并不明显,但仍然可使用贪心算法解决。 + +最优子结构已经在动态规划章节中介绍过,不再赘述,我们主要探究如何判断问题的贪心选择性质。虽然贪心选择性质的描述看上去比较简单,**但实际上对于许多问题,证明贪心选择性质不是一件易事**。 + +例如零钱兑换问题,我们虽然能够容易地举出反例,对贪心选择性质进行证伪。但如果问:**满足什么条件的硬币组合可以使用贪心算法求解**?我们往往只能凭借直觉或穷举例子来给出一个模棱两可的答案,而难以给出严谨的数学证明。 + +!!! quote + + 一篇论文 Pearson, David. "A polynomial-time algorithm for the change-making problem." *Operations Research Letters* 33.3 (2005): 231-234. 专门讨论了该问题。作者给出了一个 $O(n^3)$ 时间复杂度的算法,用于判断一个硬币组合是否可以使用贪心算法找出任何金额的最优解。 + +## 贪心解题步骤 + +贪心问题的解决流程大体可分为三步: + +1. **问题分析**:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。这一步在回溯和动态规划中都有涉及。 +2. **确定贪心策略**:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终能解决整个问题。 +3. **正确性证明**:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要使用到数学证明,例如归纳法或反证法等。 + +确定贪心策略是求解问题的核心步骤,但实施起来并没有那么容易。主要有两方面原因: + +1. **不同问题的贪心策略的差异较大**。对于许多问题来说,贪心策略都比较浅显,我们通过一些大概的思考与尝试就能得出。而对于一些复杂问题,贪心策略可能非常隐蔽,这种情况就非常考验个人的解题经验与算法能力了。 +2. **某些贪心策略具有较强的迷惑性**。当我们满怀信心设计好贪心策略,写出解题代码并提交运行,很可能发现部分测试样例无法通过。这是因为设计的贪心策略只是“部分正确”的,上文介绍的零钱兑换就是个很好的例子。 + +为了保证正确性,我们应该对贪心策略进行严谨的数学证明,**通常需要用到反证法或数学归纳法**。 + +然而,正确性证明往往也不是一件易事。如若没有头绪,我们通常会选择面向测试用例进行 Debug ,一步步修改与验证贪心策略。 + +## 贪心典型例题 + +贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下是一些典型的贪心算法问题: + +1. **硬币找零问题**:在某些硬币组合下,贪心算法总是可以得到最优解。 +2. **区间调度问题**:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。 +3. **分数背包问题**:给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值/重量)的物品,那么贪心算法在一些情况下可以得到最优解。 +4. **股票买卖问题**:给定一组股票的历史价格,你可以进行多次买卖,但不能同时参与多次交易,这意味着如果你已经持有股票,那么在卖出之前不能再买,你的目标是获取最大利润。这类问题通常可以使用贪心算法解决。 +5. **霍夫曼编码**:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最小的两个节点合并,最后得到的霍夫曼树的带权路径长度(即编码长度)最小。 +6. **Dijkstra 算法**:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。 diff --git a/docs/chapter_greedy/index.md b/docs/chapter_greedy/index.md new file mode 100644 index 000000000..209403f09 --- /dev/null +++ b/docs/chapter_greedy/index.md @@ -0,0 +1,7 @@ +# 贪心 + +
+ +![贪心](../assets/covers/chapter_greedy.jpg){ width="70%" } + +
diff --git a/mkdocs.yml b/mkdocs.yml index 4417487f7..c6224906f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -262,10 +262,17 @@ nav: - 14.6.   编辑距离问题: chapter_dynamic_programming/edit_distance_problem.md # [status: new] - 14.7.   小结: chapter_dynamic_programming/summary.md - - 15.   附录: + - 15.   贪心: + # [icon: material/head-heart-outline, status: new] + - chapter_greedy/index.md + # [status: new] + - 15.1.   贪心算法: chapter_greedy/greedy_algorithm.md + # [status: new] + - 15.2.   分数背包问题: chapter_greedy/fractional_knapsack_problem.md + - 16.   附录: # [icon: material/help-circle-outline] - chapter_appendix/index.md - - 15.1.   编程环境安装: chapter_appendix/installation.md - - 15.2.   一起参与创作: chapter_appendix/contribution.md + - 16.1.   编程环境安装: chapter_appendix/installation.md + - 16.2.   一起参与创作: chapter_appendix/contribution.md - 参考文献: - chapter_reference/index.md