This commit is contained in:
krahets 2023-08-27 00:49:58 +08:00
parent 42a06dc6bb
commit eaf77e6ac7
9 changed files with 117 additions and 109 deletions

View file

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

View file

@ -252,12 +252,12 @@ $$
<p align="center"> 图 14-7 &nbsp; 爬楼梯最小代价的动态规划过程 </p>
本题也可以进行状态压缩,将一维压缩至零维,使得空间复杂度从 $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<int> &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<int> 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] };

View file

@ -895,7 +895,7 @@ $$
<p align="center"> 图 14-16 &nbsp; 最小路径和的动态规划过程 </p>
### 4. &nbsp; 状态压缩
### 4. &nbsp; 空间优化
由于每个格子只与其左边和上边的格子有关,因此我们可以只用一个单行数组来实现 $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<vector<int>> &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<List<int>> 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<Vec<i32>>) -> i32 {
let (n, m) = (grid.len(), grid[0].len());
// 初始化 dp 表

View file

@ -415,7 +415,7 @@ $$
<p align="center"> 图 14-30 &nbsp; 编辑距离的动态规划过程 </p>
### 3. &nbsp; 状态压缩
### 3. &nbsp; 空间优化
由于 $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<int> 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<int> 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];

View file

@ -1169,14 +1169,14 @@ $$
- 将最小子问题对应的状态(即第 $1$ , $2$ 阶楼梯)称为「初始状态」。
- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」。
## 14.1.4 &nbsp; 状态压缩
## 14.1.4 &nbsp; 空间优化
细心的你可能发现,**由于 $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)$ 。
**这种空间优化技巧被称为「状态压缩」**。常见的动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。
在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时我们可以只保留必要的状态,通过“降维”来节省内存空间。**这种空间优化技巧被称为“滚动变量”或“滚动数组”**。

View file

@ -826,11 +826,11 @@ $$
<p align="center"> 图 14-20 &nbsp; 0-1 背包的动态规划过程 </p>
### 4. &nbsp; 状态压缩
### 4. &nbsp; 空间优化
由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 $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)
<p align="center"> 图 14-21 &nbsp; 0-1 背包的状态压缩后的动态规划过程 </p>
<p align="center"> 图 14-21 &nbsp; 0-1 背包的空间优化后的动态规划过程 </p>
在代码实现中,我们仅需将数组 `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<int> &wgt, vector<int> &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<int> wgt, List<int> 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 表

View file

@ -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]$ 时,无须编辑当前字符。
- 在编辑距离中,状态依赖于其正上方、正左方、左上方的状态,因此状态压缩后正序或倒序遍历都无法正确地进行状态转移。为此,我们利用一个变量暂存左上方状态,从而转化到与完全背包等价的情况,可以在状态压缩后进行正序遍历。
- 在编辑距离中,状态依赖于其正上方、正左方、左上方的状态,因此空间优化后正序或倒序遍历都无法正确地进行状态转移。为此,我们利用一个变量暂存左上方状态,从而转化到与完全背包等价的情况,可以在空间优化后进行正序遍历。

View file

@ -272,14 +272,14 @@ $$
}
```
### 3. &nbsp; 状态压缩
### 3. &nbsp; 空间优化
由于当前状态是从左边和上边的状态转移而来,**因此状态压缩后应该对 $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)
<p align="center"> 图 14-23 &nbsp; 完全背包的状态压缩后的动态规划过程 </p>
<p align="center"> 图 14-23 &nbsp; 完全背包的空间优化后的动态规划过程 </p>
代码实现比较简单,仅需将数组 `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<int> &wgt, vector<int> &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<int> wgt, List<int> 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 @@ $$
<p align="center"> 图 14-25 &nbsp; 零钱兑换问题的动态规划过程 </p>
### 3. &nbsp; 状态压缩
### 3. &nbsp; 空间优化
零钱兑换的状态压缩的处理方式和完全背包一致。
零钱兑换的空间优化的处理方式和完全背包一致。
=== "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<int> &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<int> 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. &nbsp; 状态压缩
### 3. &nbsp; 空间优化
状态压缩处理方式相同,删除硬币维度即可。
空间优化处理方式相同,删除硬币维度即可。
=== "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<int> &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<int> 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 表

View file

@ -6,17 +6,25 @@ comments: true
在某些情况下,我们希望使用一个列表的所有元素来构建一个堆,这个过程被称为“建堆操作”。
## 8.2.1 &nbsp; 借助入堆方法实现
## 8.2.1 &nbsp; 自上而下构建
最直接的方法是借助“元素入堆操作”实现。我们首先创建一个空堆,然后将列表元素依次执行“入堆”
我们首先创建一个空堆,然后遍历列表,依次对每个元素执行“入堆操作”,即先将元素添加至堆的尾部,再对该元素执行“从底至顶”堆化
设元素数量为 $n$ ,入堆操作使用 $O(\log{n})$ 时间,因此将所有元素入堆的时间复杂度为 $O(n \log n)$
每当一个元素入堆,堆的长度就加一,因此堆是“自上而下”地构建的
## 8.2.2 &nbsp; 基于堆化操作实现
设元素数量为 $n$ ,每个元素的入堆操作使用 $O(\log{n})$ 时间,因此该建堆方法的时间复杂度为 $O(n \log n)$ 。
有趣的是,存在一种更高效的建堆方法,其时间复杂度可以达到 $O(n)$ 。我们先将列表所有元素原封不动添加到堆中,然后倒序遍历该堆,依次对每个节点执行“从顶至底堆化”。
## 8.2.2 &nbsp; 自下而上构建
请注意,因为叶节点没有子节点,所以无须堆化。在代码实现中,我们从最后一个节点的父节点开始进行堆化。
实际上,我们可以实现一种更为高效的建堆方法,共分为两步。
1. 将列表所有元素原封不动添加到堆中。
2. 倒序遍历堆(即层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。
在倒序遍历中,堆是“自下而上”地构建的,需要重点理解以下两点。
- 由于叶节点没有子节点,因此无需对它们执行堆化。最后一个节点的父节点是最后一个非叶节点。
- 在倒序遍历中,我们能够保证当前节点之下的子树已经完成堆化(已经是合法的堆),而这是堆化当前节点的前置条件。
=== "Java"
@ -195,26 +203,26 @@ comments: true
## 8.2.3 &nbsp; 复杂度分析
为什么第二种建堆方法的时间复杂度是 $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)
<p align="center"> 图 8-5 &nbsp; 完美二叉树的各层节点数量 </p>
如图 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}