diff --git a/chapter_data_structure/basic_data_types.md b/chapter_data_structure/basic_data_types.md index 225ba7f75..390c0fb57 100644 --- a/chapter_data_structure/basic_data_types.md +++ b/chapter_data_structure/basic_data_types.md @@ -49,7 +49,7 @@ comments: true 如果想要表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 `int` 、小数 `float` 、还是字符 `char` ,则与“数据结构”无关。 -换句话说,**基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”**。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型(`int` , `float` , `chat`, `bool`)。 +换句话说,**基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”**。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型(`int` , `float` , `char`, `bool`)。 === "Java" diff --git a/chapter_dynamic_programming/dp_problem_features.md b/chapter_dynamic_programming/dp_problem_features.md index e4a0160b1..c66b2ff8a 100644 --- a/chapter_dynamic_programming/dp_problem_features.md +++ b/chapter_dynamic_programming/dp_problem_features.md @@ -252,12 +252,12 @@ $$

图 14-7   爬楼梯最小代价的动态规划过程

-本题也可以进行状态压缩,将一维压缩至零维,使得空间复杂度从 $O(n)$ 降低至 $O(1)$ 。 +本题也可以进行空间优化,将一维压缩至零维,使得空间复杂度从 $O(n)$ 降低至 $O(1)$ 。 === "Java" ```java title="min_cost_climbing_stairs_dp.java" - /* 爬楼梯最小代价:状态压缩后的动态规划 */ + /* 爬楼梯最小代价:空间优化后的动态规划 */ int minCostClimbingStairsDPComp(int[] cost) { int n = cost.length - 1; if (n == 1 || n == 2) @@ -275,7 +275,7 @@ $$ === "C++" ```cpp title="min_cost_climbing_stairs_dp.cpp" - /* 爬楼梯最小代价:状态压缩后的动态规划 */ + /* 爬楼梯最小代价:空间优化后的动态规划 */ int minCostClimbingStairsDPComp(vector &cost) { int n = cost.size() - 1; if (n == 1 || n == 2) @@ -294,7 +294,7 @@ $$ ```python title="min_cost_climbing_stairs_dp.py" def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int: - """爬楼梯最小代价:状态压缩后的动态规划""" + """爬楼梯最小代价:空间优化后的动态规划""" n = len(cost) - 1 if n == 1 or n == 2: return cost[n] @@ -307,7 +307,7 @@ $$ === "Go" ```go title="min_cost_climbing_stairs_dp.go" - /* 爬楼梯最小代价:状态压缩后的动态规划 */ + /* 爬楼梯最小代价:空间优化后的动态规划 */ func minCostClimbingStairsDPComp(cost []int) int { n := len(cost) - 1 if n == 1 || n == 2 { @@ -346,7 +346,7 @@ $$ === "C#" ```csharp title="min_cost_climbing_stairs_dp.cs" - /* 爬楼梯最小代价:状态压缩后的动态规划 */ + /* 爬楼梯最小代价:空间优化后的动态规划 */ int minCostClimbingStairsDPComp(int[] cost) { int n = cost.Length - 1; if (n == 1 || n == 2) @@ -364,7 +364,7 @@ $$ === "Swift" ```swift title="min_cost_climbing_stairs_dp.swift" - /* 爬楼梯最小代价:状态压缩后的动态规划 */ + /* 爬楼梯最小代价:空间优化后的动态规划 */ func minCostClimbingStairsDPComp(cost: [Int]) -> Int { let n = cost.count - 1 if n == 1 || n == 2 { @@ -381,7 +381,7 @@ $$ === "Zig" ```zig title="min_cost_climbing_stairs_dp.zig" - // 爬楼梯最小代价:状态压缩后的动态规划 + // 爬楼梯最小代价:空间优化后的动态规划 fn minCostClimbingStairsDPComp(cost: []i32) i32 { var n = cost.len - 1; if (n == 1 or n == 2) { @@ -402,7 +402,7 @@ $$ === "Dart" ```dart title="min_cost_climbing_stairs_dp.dart" - /* 爬楼梯最小代价:状态压缩后的动态规划 */ + /* 爬楼梯最小代价:空间优化后的动态规划 */ int minCostClimbingStairsDPComp(List cost) { int n = cost.length - 1; if (n == 1 || n == 2) return cost[n]; @@ -419,7 +419,7 @@ $$ === "Rust" ```rust title="min_cost_climbing_stairs_dp.rs" - /* 爬楼梯最小代价:状态压缩后的动态规划 */ + /* 爬楼梯最小代价:空间优化后的动态规划 */ fn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 { let n = cost.len() - 1; if n == 1 || n == 2 { return cost[n] }; diff --git a/chapter_dynamic_programming/dp_solution_pipeline.md b/chapter_dynamic_programming/dp_solution_pipeline.md index b456d093d..48ab140bc 100644 --- a/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/chapter_dynamic_programming/dp_solution_pipeline.md @@ -895,7 +895,7 @@ $$

图 14-16   最小路径和的动态规划过程

-### 4.   状态压缩 +### 4.   空间优化 由于每个格子只与其左边和上边的格子有关,因此我们可以只用一个单行数组来实现 $dp$ 表。 @@ -904,7 +904,7 @@ $$ === "Java" ```java title="min_path_sum.java" - /* 最小路径和:状态压缩后的动态规划 */ + /* 最小路径和:空间优化后的动态规划 */ int minPathSumDPComp(int[][] grid) { int n = grid.length, m = grid[0].length; // 初始化 dp 表 @@ -930,7 +930,7 @@ $$ === "C++" ```cpp title="min_path_sum.cpp" - /* 最小路径和:状态压缩后的动态规划 */ + /* 最小路径和:空间优化后的动态规划 */ int minPathSumDPComp(vector> &grid) { int n = grid.size(), m = grid[0].size(); // 初始化 dp 表 @@ -957,7 +957,7 @@ $$ ```python title="min_path_sum.py" def min_path_sum_dp_comp(grid: list[list[int]]) -> int: - """最小路径和:状态压缩后的动态规划""" + """最小路径和:空间优化后的动态规划""" n, m = len(grid), len(grid[0]) # 初始化 dp 表 dp = [0] * m @@ -978,7 +978,7 @@ $$ === "Go" ```go title="min_path_sum.go" - /* 最小路径和:状态压缩后的动态规划 */ + /* 最小路径和:空间优化后的动态规划 */ func minPathSumDPComp(grid [][]int) int { n, m := len(grid), len(grid[0]) // 初始化 dp 表 @@ -1022,7 +1022,7 @@ $$ === "C#" ```csharp title="min_path_sum.cs" - /* 最小路径和:状态压缩后的动态规划 */ + /* 最小路径和:空间优化后的动态规划 */ int minPathSumDPComp(int[][] grid) { int n = grid.Length, m = grid[0].Length; // 初始化 dp 表 @@ -1048,7 +1048,7 @@ $$ === "Swift" ```swift title="min_path_sum.swift" - /* 最小路径和:状态压缩后的动态规划 */ + /* 最小路径和:空间优化后的动态规划 */ func minPathSumDPComp(grid: [[Int]]) -> Int { let n = grid.count let m = grid[0].count @@ -1075,7 +1075,7 @@ $$ === "Zig" ```zig title="min_path_sum.zig" - // 最小路径和:状态压缩后的动态规划 + // 最小路径和:空间优化后的动态规划 fn minPathSumDPComp(comptime grid: anytype) i32 { comptime var n = grid.len; comptime var m = grid[0].len; @@ -1101,7 +1101,7 @@ $$ === "Dart" ```dart title="min_path_sum.dart" - /* 最小路径和:状态压缩后的动态规划 */ + /* 最小路径和:空间优化后的动态规划 */ int minPathSumDPComp(List> grid) { int n = grid.length, m = grid[0].length; // 初始化 dp 表 @@ -1126,7 +1126,7 @@ $$ === "Rust" ```rust title="min_path_sum.rs" - /* 最小路径和:状态压缩后的动态规划 */ + /* 最小路径和:空间优化后的动态规划 */ fn min_path_sum_dp_comp(grid: &Vec>) -> i32 { let (n, m) = (grid.len(), grid[0].len()); // 初始化 dp 表 diff --git a/chapter_dynamic_programming/edit_distance_problem.md b/chapter_dynamic_programming/edit_distance_problem.md index 682fbe531..9c926ddfa 100644 --- a/chapter_dynamic_programming/edit_distance_problem.md +++ b/chapter_dynamic_programming/edit_distance_problem.md @@ -415,7 +415,7 @@ $$

图 14-30   编辑距离的动态规划过程

-### 3.   状态压缩 +### 3.   空间优化 由于 $dp[i,j]$ 是由上方 $dp[i-1, j]$ 、左方 $dp[i, j-1]$ 、左上方状态 $dp[i-1, j-1]$ 转移而来,而正序遍历会丢失左上方 $dp[i-1, j-1]$ ,倒序遍历无法提前构建 $dp[i, j-1]$ ,因此两种遍历顺序都不可取。 @@ -424,7 +424,7 @@ $$ === "Java" ```java title="edit_distance.java" - /* 编辑距离:状态压缩后的动态规划 */ + /* 编辑距离:空间优化后的动态规划 */ int editDistanceDPComp(String s, String t) { int n = s.length(), m = t.length(); int[] dp = new int[m + 1]; @@ -457,7 +457,7 @@ $$ === "C++" ```cpp title="edit_distance.cpp" - /* 编辑距离:状态压缩后的动态规划 */ + /* 编辑距离:空间优化后的动态规划 */ int editDistanceDPComp(string s, string t) { int n = s.length(), m = t.length(); vector dp(m + 1, 0); @@ -491,7 +491,7 @@ $$ ```python title="edit_distance.py" def edit_distance_dp_comp(s: str, t: str) -> int: - """编辑距离:状态压缩后的动态规划""" + """编辑距离:空间优化后的动态规划""" n, m = len(s), len(t) dp = [0] * (m + 1) # 状态转移:首行 @@ -518,7 +518,7 @@ $$ === "Go" ```go title="edit_distance.go" - /* 编辑距离:状态压缩后的动态规划 */ + /* 编辑距离:空间优化后的动态规划 */ func editDistanceDPComp(s string, t string) int { n := len(s) m := len(t) @@ -570,7 +570,7 @@ $$ === "C#" ```csharp title="edit_distance.cs" - /* 编辑距离:状态压缩后的动态规划 */ + /* 编辑距离:空间优化后的动态规划 */ int editDistanceDPComp(string s, string t) { int n = s.Length, m = t.Length; int[] dp = new int[m + 1]; @@ -603,7 +603,7 @@ $$ === "Swift" ```swift title="edit_distance.swift" - /* 编辑距离:状态压缩后的动态规划 */ + /* 编辑距离:空间优化后的动态规划 */ func editDistanceDPComp(s: String, t: String) -> Int { let n = s.utf8CString.count let m = t.utf8CString.count @@ -637,7 +637,7 @@ $$ === "Zig" ```zig title="edit_distance.zig" - // 编辑距离:状态压缩后的动态规划 + // 编辑距离:空间优化后的动态规划 fn editDistanceDPComp(comptime s: []const u8, comptime t: []const u8) i32 { comptime var n = s.len; comptime var m = t.len; @@ -671,7 +671,7 @@ $$ === "Dart" ```dart title="edit_distance.dart" - /* 编辑距离:状态压缩后的动态规划 */ + /* 编辑距离:空间优化后的动态规划 */ int editDistanceDPComp(String s, String t) { int n = s.length, m = t.length; List dp = List.filled(m + 1, 0); @@ -704,7 +704,7 @@ $$ === "Rust" ```rust title="edit_distance.rs" - /* 编辑距离:状态压缩后的动态规划 */ + /* 编辑距离:空间优化后的动态规划 */ fn edit_distance_dp_comp(s: &str, t: &str) -> i32 { let (n, m) = (s.len(), t.len()); let mut dp = vec![0; m + 1]; diff --git a/chapter_dynamic_programming/intro_to_dynamic_programming.md b/chapter_dynamic_programming/intro_to_dynamic_programming.md index 727047045..6571d6133 100644 --- a/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -1169,14 +1169,14 @@ $$ - 将最小子问题对应的状态(即第 $1$ , $2$ 阶楼梯)称为「初始状态」。 - 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」。 -## 14.1.4   状态压缩 +## 14.1.4   空间优化 细心的你可能发现,**由于 $dp[i]$ 只与 $dp[i-1]$ 和 $dp[i-2]$ 有关,因此我们无须使用一个数组 `dp` 来存储所有子问题的解**,而只需两个变量滚动前进即可。 === "Java" ```java title="climbing_stairs_dp.java" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ int climbingStairsDPComp(int n) { if (n == 1 || n == 2) return n; @@ -1193,7 +1193,7 @@ $$ === "C++" ```cpp title="climbing_stairs_dp.cpp" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ int climbingStairsDPComp(int n) { if (n == 1 || n == 2) return n; @@ -1211,7 +1211,7 @@ $$ ```python title="climbing_stairs_dp.py" def climbing_stairs_dp_comp(n: int) -> int: - """爬楼梯:状态压缩后的动态规划""" + """爬楼梯:空间优化后的动态规划""" if n == 1 or n == 2: return n a, b = 1, 2 @@ -1223,7 +1223,7 @@ $$ === "Go" ```go title="climbing_stairs_dp.go" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ func climbingStairsDPComp(n int) int { if n == 1 || n == 2 { return n @@ -1240,7 +1240,7 @@ $$ === "JS" ```javascript title="climbing_stairs_dp.js" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ function climbingStairsDPComp(n) { if (n === 1 || n === 2) return n; let a = 1, @@ -1257,7 +1257,7 @@ $$ === "TS" ```typescript title="climbing_stairs_dp.ts" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ function climbingStairsDPComp(n: number): number { if (n === 1 || n === 2) return n; let a = 1, @@ -1280,7 +1280,7 @@ $$ === "C#" ```csharp title="climbing_stairs_dp.cs" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ int climbingStairsDPComp(int n) { if (n == 1 || n == 2) return n; @@ -1297,7 +1297,7 @@ $$ === "Swift" ```swift title="climbing_stairs_dp.swift" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ func climbingStairsDPComp(n: Int) -> Int { if n == 1 || n == 2 { return n @@ -1314,7 +1314,7 @@ $$ === "Zig" ```zig title="climbing_stairs_dp.zig" - // 爬楼梯:状态压缩后的动态规划 + // 爬楼梯:空间优化后的动态规划 fn climbingStairsDPComp(comptime n: usize) i32 { if (n == 1 or n == 2) { return @intCast(n); @@ -1333,7 +1333,7 @@ $$ === "Dart" ```dart title="climbing_stairs_dp.dart" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ int climbingStairsDPComp(int n) { if (n == 1 || n == 2) return n; int a = 1, b = 2; @@ -1349,7 +1349,7 @@ $$ === "Rust" ```rust title="climbing_stairs_dp.rs" - /* 爬楼梯:状态压缩后的动态规划 */ + /* 爬楼梯:空间优化后的动态规划 */ fn climbing_stairs_dp_comp(n: usize) -> i32 { if n == 1 || n == 2 { return n as i32; } let (mut a, mut b) = (1, 2); @@ -1364,4 +1364,4 @@ $$ 观察以上代码,由于省去了数组 `dp` 占用的空间,因此空间复杂度从 $O(n)$ 降低至 $O(1)$ 。 -**这种空间优化技巧被称为「状态压缩」**。在常见的动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。 +在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时我们可以只保留必要的状态,通过“降维”来节省内存空间。**这种空间优化技巧被称为“滚动变量”或“滚动数组”**。 diff --git a/chapter_dynamic_programming/knapsack_problem.md b/chapter_dynamic_programming/knapsack_problem.md index 611f2ae12..43ab65956 100644 --- a/chapter_dynamic_programming/knapsack_problem.md +++ b/chapter_dynamic_programming/knapsack_problem.md @@ -826,11 +826,11 @@ $$

图 14-20   0-1 背包的动态规划过程

-### 4.   状态压缩 +### 4.   空间优化 由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 $O(n^2)$ 将低至 $O(n)$ 。 -进一步思考,我们是否可以仅用一个数组实现状态压缩呢?观察可知,每个状态都是由正上方或左上方的格子转移过来的。假设只有一个数组,当开始遍历第 $i$ 行时,该数组存储的仍然是第 $i-1$ 行的状态。 +进一步思考,我们是否可以仅用一个数组实现空间优化呢?观察可知,每个状态都是由正上方或左上方的格子转移过来的。假设只有一个数组,当开始遍历第 $i$ 行时,该数组存储的仍然是第 $i-1$ 行的状态。 - 如果采取正序遍历,那么遍历到 $dp[i, j]$ 时,左上方 $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ 值可能已经被覆盖,此时就无法得到正确的状态转移结果。 - 如果采取倒序遍历,则不会发生覆盖问题,状态转移可以正确进行。 @@ -838,7 +838,7 @@ $$ 图 14-21 展示了在单个数组下从第 $i = 1$ 行转换至第 $i = 2$ 行的过程。请思考正序遍历和倒序遍历的区别。 === "<1>" - ![0-1 背包的状态压缩后的动态规划过程](knapsack_problem.assets/knapsack_dp_comp_step1.png) + ![0-1 背包的空间优化后的动态规划过程](knapsack_problem.assets/knapsack_dp_comp_step1.png) === "<2>" ![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png) @@ -855,14 +855,14 @@ $$ === "<6>" ![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png) -

图 14-21   0-1 背包的状态压缩后的动态规划过程

+

图 14-21   0-1 背包的空间优化后的动态规划过程

在代码实现中,我们仅需将数组 `dp` 的第一维 $i$ 直接删除,并且把内循环更改为倒序遍历即可。 === "Java" ```java title="knapsack.java" - /* 0-1 背包:状态压缩后的动态规划 */ + /* 0-1 背包:空间优化后的动态规划 */ int knapsackDPComp(int[] wgt, int[] val, int cap) { int n = wgt.length; // 初始化 dp 表 @@ -884,7 +884,7 @@ $$ === "C++" ```cpp title="knapsack.cpp" - /* 0-1 背包:状态压缩后的动态规划 */ + /* 0-1 背包:空间优化后的动态规划 */ int knapsackDPComp(vector &wgt, vector &val, int cap) { int n = wgt.size(); // 初始化 dp 表 @@ -907,7 +907,7 @@ $$ ```python title="knapsack.py" def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: - """0-1 背包:状态压缩后的动态规划""" + """0-1 背包:空间优化后的动态规划""" n = len(wgt) # 初始化 dp 表 dp = [0] * (cap + 1) @@ -927,7 +927,7 @@ $$ === "Go" ```go title="knapsack.go" - /* 0-1 背包:状态压缩后的动态规划 */ + /* 0-1 背包:空间优化后的动态规划 */ func knapsackDPComp(wgt, val []int, cap int) int { n := len(wgt) // 初始化 dp 表 @@ -967,7 +967,7 @@ $$ === "C#" ```csharp title="knapsack.cs" - /* 0-1 背包:状态压缩后的动态规划 */ + /* 0-1 背包:空间优化后的动态规划 */ int knapsackDPComp(int[] weight, int[] val, int cap) { int n = weight.Length; // 初始化 dp 表 @@ -992,7 +992,7 @@ $$ === "Swift" ```swift title="knapsack.swift" - /* 0-1 背包:状态压缩后的动态规划 */ + /* 0-1 背包:空间优化后的动态规划 */ func knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { let n = wgt.count // 初始化 dp 表 @@ -1014,7 +1014,7 @@ $$ === "Zig" ```zig title="knapsack.zig" - // 0-1 背包:状态压缩后的动态规划 + // 0-1 背包:空间优化后的动态规划 fn knapsackDPComp(wgt: []i32, val: []i32, comptime cap: usize) i32 { var n = wgt.len; // 初始化 dp 表 @@ -1037,7 +1037,7 @@ $$ === "Dart" ```dart title="knapsack.dart" - /* 0-1 背包:状态压缩后的动态规划 */ + /* 0-1 背包:空间优化后的动态规划 */ int knapsackDPComp(List wgt, List val, int cap) { int n = wgt.length; // 初始化 dp 表 @@ -1059,7 +1059,7 @@ $$ === "Rust" ```rust title="knapsack.rs" - /* 0-1 背包:状态压缩后的动态规划 */ + /* 0-1 背包:空间优化后的动态规划 */ fn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { let n = wgt.len(); // 初始化 dp 表 diff --git a/chapter_dynamic_programming/summary.md b/chapter_dynamic_programming/summary.md index 314f37e36..3da712dd4 100644 --- a/chapter_dynamic_programming/summary.md +++ b/chapter_dynamic_programming/summary.md @@ -16,8 +16,8 @@ status: new **背包问题** - 背包问题是最典型的动态规划题目,具有 0-1 背包、完全背包、多重背包等变种问题。 -- 0-1 背包的状态定义为前 $i$ 个物品在剩余容量为 $c$ 的背包中的最大价值。根据不放入背包和放入背包两种决策,可得到最优子结构,并构建出状态转移方程。在状态压缩中,由于每个状态依赖正上方和左上方的状态,因此需要倒序遍历列表,避免左上方状态被覆盖。 -- 完全背包的每种物品的选取数量无限制,因此选择放入物品的状态转移与 0-1 背包不同。由于状态依赖于正上方和正左方的状态,因此在状态压缩中应当正序遍历。 +- 0-1 背包的状态定义为前 $i$ 个物品在剩余容量为 $c$ 的背包中的最大价值。根据不放入背包和放入背包两种决策,可得到最优子结构,并构建出状态转移方程。在空间优化中,由于每个状态依赖正上方和左上方的状态,因此需要倒序遍历列表,避免左上方状态被覆盖。 +- 完全背包的每种物品的选取数量无限制,因此选择放入物品的状态转移与 0-1 背包不同。由于状态依赖于正上方和正左方的状态,因此在空间优化中应当正序遍历。 - 零钱兑换问题是完全背包的一个变种。它从求“最大”价值变为求“最小”硬币数量,因此状态转移方程中的 $\max()$ 应改为 $\min()$ 。从求“不超过”背包容量到求“恰好”凑出目标金额,因此使用 $amt + 1$ 来表示“无法凑出目标金额”的无效解。 - 零钱兑换 II 问题从求“最少硬币数量”改为求“硬币组合数量”,状态转移方程相应地从 $\min()$ 改为求和运算符。 @@ -25,4 +25,4 @@ status: new - 编辑距离(Levenshtein 距离)用于衡量两个字符串之间的相似度,其定义为从一个字符串到另一个字符串的最小编辑步数,编辑操作包括添加、删除、替换。 - 编辑距离问题的状态定义为将 $s$ 的前 $i$ 个字符更改为 $t$ 的前 $j$ 个字符所需的最少编辑步数。当 $s[i] \ne t[j]$ 时,具有三种决策:添加、删除、替换,它们都有相应的剩余子问题。据此便可以找出最优子结构与构建状态转移方程。而当 $s[i] = t[j]$ 时,无须编辑当前字符。 -- 在编辑距离中,状态依赖于其正上方、正左方、左上方的状态,因此状态压缩后正序或倒序遍历都无法正确地进行状态转移。为此,我们利用一个变量暂存左上方状态,从而转化到与完全背包等价的情况,可以在状态压缩后进行正序遍历。 +- 在编辑距离中,状态依赖于其正上方、正左方、左上方的状态,因此空间优化后正序或倒序遍历都无法正确地进行状态转移。为此,我们利用一个变量暂存左上方状态,从而转化到与完全背包等价的情况,可以在空间优化后进行正序遍历。 diff --git a/chapter_dynamic_programming/unbounded_knapsack_problem.md b/chapter_dynamic_programming/unbounded_knapsack_problem.md index 2908565e8..fd40a01a1 100644 --- a/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -272,14 +272,14 @@ $$ } ``` -### 3.   状态压缩 +### 3.   空间优化 -由于当前状态是从左边和上边的状态转移而来,**因此状态压缩后应该对 $dp$ 表中的每一行采取正序遍历**。 +由于当前状态是从左边和上边的状态转移而来,**因此空间优化后应该对 $dp$ 表中的每一行采取正序遍历**。 这个遍历顺序与 0-1 背包正好相反。请借助图 14-23 来理解两者的区别。 === "<1>" - ![完全背包的状态压缩后的动态规划过程](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png) + ![完全背包的空间优化后的动态规划过程](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png) === "<2>" ![unbounded_knapsack_dp_comp_step2](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step2.png) @@ -296,14 +296,14 @@ $$ === "<6>" ![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png) -

图 14-23   完全背包的状态压缩后的动态规划过程

+

图 14-23   完全背包的空间优化后的动态规划过程

代码实现比较简单,仅需将数组 `dp` 的第一维删除。 === "Java" ```java title="unbounded_knapsack.java" - /* 完全背包:状态压缩后的动态规划 */ + /* 完全背包:空间优化后的动态规划 */ int unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { int n = wgt.length; // 初始化 dp 表 @@ -327,7 +327,7 @@ $$ === "C++" ```cpp title="unbounded_knapsack.cpp" - /* 完全背包:状态压缩后的动态规划 */ + /* 完全背包:空间优化后的动态规划 */ int unboundedKnapsackDPComp(vector &wgt, vector &val, int cap) { int n = wgt.size(); // 初始化 dp 表 @@ -352,7 +352,7 @@ $$ ```python title="unbounded_knapsack.py" def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: - """完全背包:状态压缩后的动态规划""" + """完全背包:空间优化后的动态规划""" n = len(wgt) # 初始化 dp 表 dp = [0] * (cap + 1) @@ -372,7 +372,7 @@ $$ === "Go" ```go title="unbounded_knapsack.go" - /* 完全背包:状态压缩后的动态规划 */ + /* 完全背包:空间优化后的动态规划 */ func unboundedKnapsackDPComp(wgt, val []int, cap int) int { n := len(wgt) // 初始化 dp 表 @@ -414,7 +414,7 @@ $$ === "C#" ```csharp title="unbounded_knapsack.cs" - /* 完全背包:状态压缩后的动态规划 */ + /* 完全背包:空间优化后的动态规划 */ int unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { int n = wgt.Length; // 初始化 dp 表 @@ -438,7 +438,7 @@ $$ === "Swift" ```swift title="unbounded_knapsack.swift" - /* 完全背包:状态压缩后的动态规划 */ + /* 完全背包:空间优化后的动态规划 */ func unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { let n = wgt.count // 初始化 dp 表 @@ -462,7 +462,7 @@ $$ === "Zig" ```zig title="unbounded_knapsack.zig" - // 完全背包:状态压缩后的动态规划 + // 完全背包:空间优化后的动态规划 fn unboundedKnapsackDPComp(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { comptime var n = wgt.len; // 初始化 dp 表 @@ -486,7 +486,7 @@ $$ === "Dart" ```dart title="unbounded_knapsack.dart" - /* 完全背包:状态压缩后的动态规划 */ + /* 完全背包:空间优化后的动态规划 */ int unboundedKnapsackDPComp(List wgt, List val, int cap) { int n = wgt.length; // 初始化 dp 表 @@ -510,7 +510,7 @@ $$ === "Rust" ```rust title="unbounded_knapsack.rs" - /* 完全背包:状态压缩后的动态规划 */ + /* 完全背包:空间优化后的动态规划 */ fn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { let n = wgt.len(); // 初始化 dp 表 @@ -915,14 +915,14 @@ $$

图 14-25   零钱兑换问题的动态规划过程

-### 3.   状态压缩 +### 3.   空间优化 -零钱兑换的状态压缩的处理方式和完全背包一致。 +零钱兑换的空间优化的处理方式和完全背包一致。 === "Java" ```java title="coin_change.java" - /* 零钱兑换:状态压缩后的动态规划 */ + /* 零钱兑换:空间优化后的动态规划 */ int coinChangeDPComp(int[] coins, int amt) { int n = coins.length; int MAX = amt + 1; @@ -949,7 +949,7 @@ $$ === "C++" ```cpp title="coin_change.cpp" - /* 零钱兑换:状态压缩后的动态规划 */ + /* 零钱兑换:空间优化后的动态规划 */ int coinChangeDPComp(vector &coins, int amt) { int n = coins.size(); int MAX = amt + 1; @@ -976,7 +976,7 @@ $$ ```python title="coin_change.py" def coin_change_dp_comp(coins: list[int], amt: int) -> int: - """零钱兑换:状态压缩后的动态规划""" + """零钱兑换:空间优化后的动态规划""" n = len(coins) MAX = amt + 1 # 初始化 dp 表 @@ -1048,7 +1048,7 @@ $$ === "C#" ```csharp title="coin_change.cs" - /* 零钱兑换:状态压缩后的动态规划 */ + /* 零钱兑换:空间优化后的动态规划 */ int coinChangeDPComp(int[] coins, int amt) { int n = coins.Length; int MAX = amt + 1; @@ -1075,7 +1075,7 @@ $$ === "Swift" ```swift title="coin_change.swift" - /* 零钱兑换:状态压缩后的动态规划 */ + /* 零钱兑换:空间优化后的动态规划 */ func coinChangeDPComp(coins: [Int], amt: Int) -> Int { let n = coins.count let MAX = amt + 1 @@ -1101,7 +1101,7 @@ $$ === "Zig" ```zig title="coin_change.zig" - // 零钱兑换:状态压缩后的动态规划 + // 零钱兑换:空间优化后的动态规划 fn coinChangeDPComp(comptime coins: []i32, comptime amt: usize) i32 { comptime var n = coins.len; comptime var max = amt + 1; @@ -1132,7 +1132,7 @@ $$ === "Dart" ```dart title="coin_change.dart" - /* 零钱兑换:状态压缩后的动态规划 */ + /* 零钱兑换:空间优化后的动态规划 */ int coinChangeDPComp(List coins, int amt) { int n = coins.length; int MAX = amt + 1; @@ -1158,7 +1158,7 @@ $$ === "Rust" ```rust title="coin_change.rs" - /* 零钱兑换:状态压缩后的动态规划 */ + /* 零钱兑换:空间优化后的动态规划 */ fn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 { let n = coins.len(); let max = amt + 1; @@ -1474,14 +1474,14 @@ $$ } ``` -### 3.   状态压缩 +### 3.   空间优化 -状态压缩处理方式相同,删除硬币维度即可。 +空间优化处理方式相同,删除硬币维度即可。 === "Java" ```java title="coin_change_ii.java" - /* 零钱兑换 II:状态压缩后的动态规划 */ + /* 零钱兑换 II:空间优化后的动态规划 */ int coinChangeIIDPComp(int[] coins, int amt) { int n = coins.length; // 初始化 dp 表 @@ -1506,7 +1506,7 @@ $$ === "C++" ```cpp title="coin_change_ii.cpp" - /* 零钱兑换 II:状态压缩后的动态规划 */ + /* 零钱兑换 II:空间优化后的动态规划 */ int coinChangeIIDPComp(vector &coins, int amt) { int n = coins.size(); // 初始化 dp 表 @@ -1532,7 +1532,7 @@ $$ ```python title="coin_change_ii.py" def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int: - """零钱兑换 II:状态压缩后的动态规划""" + """零钱兑换 II:空间优化后的动态规划""" n = len(coins) # 初始化 dp 表 dp = [0] * (amt + 1) @@ -1553,7 +1553,7 @@ $$ === "Go" ```go title="coin_change_ii.go" - /* 零钱兑换 II:状态压缩后的动态规划 */ + /* 零钱兑换 II:空间优化后的动态规划 */ func coinChangeIIDPComp(coins []int, amt int) int { n := len(coins) // 初始化 dp 表 @@ -1597,7 +1597,7 @@ $$ === "C#" ```csharp title="coin_change_ii.cs" - /* 零钱兑换 II:状态压缩后的动态规划 */ + /* 零钱兑换 II:空间优化后的动态规划 */ int coinChangeIIDPComp(int[] coins, int amt) { int n = coins.Length; // 初始化 dp 表 @@ -1622,7 +1622,7 @@ $$ === "Swift" ```swift title="coin_change_ii.swift" - /* 零钱兑换 II:状态压缩后的动态规划 */ + /* 零钱兑换 II:空间优化后的动态规划 */ func coinChangeIIDPComp(coins: [Int], amt: Int) -> Int { let n = coins.count // 初始化 dp 表 @@ -1647,7 +1647,7 @@ $$ === "Zig" ```zig title="coin_change_ii.zig" - // 零钱兑换 II:状态压缩后的动态规划 + // 零钱兑换 II:空间优化后的动态规划 fn coinChangeIIDPComp(comptime coins: []i32, comptime amt: usize) i32 { comptime var n = coins.len; // 初始化 dp 表 @@ -1672,7 +1672,7 @@ $$ === "Dart" ```dart title="coin_change_ii.dart" - /* 零钱兑换 II:状态压缩后的动态规划 */ + /* 零钱兑换 II:空间优化后的动态规划 */ int coinChangeIIDPComp(List coins, int amt) { int n = coins.length; // 初始化 dp 表 @@ -1697,7 +1697,7 @@ $$ === "Rust" ```rust title="coin_change_ii.rs" - /* 零钱兑换 II:状态压缩后的动态规划 */ + /* 零钱兑换 II:空间优化后的动态规划 */ fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 { let n = coins.len(); // 初始化 dp 表 diff --git a/chapter_heap/build_heap.md b/chapter_heap/build_heap.md index 215975697..d482706bc 100644 --- a/chapter_heap/build_heap.md +++ b/chapter_heap/build_heap.md @@ -6,17 +6,25 @@ comments: true 在某些情况下,我们希望使用一个列表的所有元素来构建一个堆,这个过程被称为“建堆操作”。 -## 8.2.1   借助入堆方法实现 +## 8.2.1   自上而下构建 -最直接的方法是借助“元素入堆操作”实现。我们首先创建一个空堆,然后将列表元素依次执行“入堆”。 +我们首先创建一个空堆,然后遍历列表,依次对每个元素执行“入堆操作”,即先将元素添加至堆的尾部,再对该元素执行“从底至顶”堆化。 -设元素数量为 $n$ ,入堆操作使用 $O(\log{n})$ 时间,因此将所有元素入堆的时间复杂度为 $O(n \log n)$ 。 +每当一个元素入堆,堆的长度就加一,因此堆是“自上而下”地构建的。 -## 8.2.2   基于堆化操作实现 +设元素数量为 $n$ ,每个元素的入堆操作使用 $O(\log{n})$ 时间,因此该建堆方法的时间复杂度为 $O(n \log n)$ 。 -有趣的是,存在一种更高效的建堆方法,其时间复杂度可以达到 $O(n)$ 。我们先将列表所有元素原封不动添加到堆中,然后倒序遍历该堆,依次对每个节点执行“从顶至底堆化”。 +## 8.2.2   自下而上构建 -请注意,因为叶节点没有子节点,所以无须堆化。在代码实现中,我们从最后一个节点的父节点开始进行堆化。 +实际上,我们可以实现一种更为高效的建堆方法,共分为两步。 + +1. 将列表所有元素原封不动添加到堆中。 +2. 倒序遍历堆(即层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。 + +在倒序遍历中,堆是“自下而上”地构建的,需要重点理解以下两点。 + +- 由于叶节点没有子节点,因此无需对它们执行堆化。最后一个节点的父节点是最后一个非叶节点。 +- 在倒序遍历中,我们能够保证当前节点之下的子树已经完成堆化(已经是合法的堆),而这是堆化当前节点的前置条件。 === "Java" @@ -195,26 +203,26 @@ comments: true ## 8.2.3   复杂度分析 -为什么第二种建堆方法的时间复杂度是 $O(n)$ ?我们来展开推算一下。 +下面,我们来尝试推算第二种建堆方法的时间复杂度。 -- 在完全二叉树中,设节点总数为 $n$ ,则叶节点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此,在排除叶节点后,需要堆化的节点数量为 $(n - 1)/2$ ,复杂度为 $O(n)$ 。 -- 在从顶至底堆化的过程中,每个节点最多堆化到叶节点,因此最大迭代次数为二叉树高度 $O(\log n)$ 。 +- 假设完全二叉树的节点数量为 $n$ ,则叶节点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此需要堆化的节点数量为 $(n - 1) / 2$ 。 +- 在从顶至底堆化的过程中,每个节点最多堆化到叶节点,因此最大迭代次数为二叉树高度 $\log n$ 。 -将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$ 。**然而,这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的特性**。 +将上述两者相乘,可得到建堆过程的时间复杂度为 $O(n \log n)$ 。**但这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的性质**。 -接下来我们来进行更为详细的计算。为了减小计算难度,我们假设树是一个“完美二叉树”,该假设不会影响计算结果的正确性。设二叉树(即堆)节点数量为 $n$ ,树高度为 $h$ 。 +接下来我们来进行更为准确的计算。为了减小计算难度,假设给定一个节点数量为 $n$ ,高度为 $h$ 的“完美二叉树”,该假设不会影响计算结果的正确性。 ![完美二叉树的各层节点数量](build_heap.assets/heapify_operations_count.png)

图 8-5   完美二叉树的各层节点数量

-如图 8-5 所示,**节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”**。因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。 +如图 8-5 所示,节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”。因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。 $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 $$ -化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,得到 +化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,得到: $$ \begin{aligned} @@ -223,13 +231,13 @@ T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline \end{aligned} $$ -使用错位相减法,用下式 $2 T(h)$ 减去上式 $T(h)$ ,可得 +使用错位相减法,用下式 $2 T(h)$ 减去上式 $T(h)$ ,可得: $$ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h $$ -观察上式,发现 $T(h)$ 是一个等比数列,可直接使用求和公式,得到时间复杂度为 +观察上式,发现 $T(h)$ 是一个等比数列,可直接使用求和公式,得到时间复杂度为: $$ \begin{aligned}