Release Rust code to documents. (#656)

This commit is contained in:
Yudong Jin
2023-07-26 11:00:53 +08:00
committed by GitHub
parent 60162f6fa8
commit 027bdd6510
61 changed files with 1155 additions and 145 deletions

View File

@@ -100,6 +100,12 @@ $$
[class]{}-[func]{minCostClimbingStairsDP}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```
![爬楼梯最小代价的动态规划过程](dp_problem_features.assets/min_cost_cs_dp.png)
本题也可以进行状态压缩,将一维压缩至零维,使得空间复杂度从 $O(n)$ 降低至 $O(1)$ 。
@@ -170,6 +176,12 @@ $$
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== "Rust"
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```
## 无后效性
「无后效性」是动态规划能够有效解决问题的重要特性之一,定义为:**给定一个确定的状态,它的未来发展只与当前状态有关,而与当前状态过去所经历过的所有状态无关**。
@@ -274,6 +286,12 @@ $$
[class]{}-[func]{climbingStairsConstraintDP}
```
=== "Rust"
```rust title="climbing_stairs_constraint_dp.rs"
[class]{}-[func]{climbing_stairs_constraint_dp}
```
在上面的案例中,由于仅需多考虑前面一个状态,我们仍然可以通过扩展状态定义,使得问题恢复无后效性。然而,许多问题具有非常严重的“有后效性”,例如:
!!! question "爬楼梯与障碍生成"

View File

@@ -164,6 +164,12 @@ $$
[class]{}-[func]{minPathSumDFS}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dfs}
```
下图给出了以 $dp[2, 1]$ 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 `grid` 的尺寸变大而急剧增多。
本质上看,造成重叠子问题的原因为:**存在多条路径可以从左上角到达某一单元格**。
@@ -242,6 +248,12 @@ $$
[class]{}-[func]{minPathSumDFSMem}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dfs_mem}
```
引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 $O(nm)$ 。
![记忆化搜索递归树](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png)
@@ -316,6 +328,12 @@ $$
[class]{}-[func]{minPathSumDP}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dp}
```
下图展示了最小路径和的状态转移过程,其遍历了整个网格,**因此时间复杂度为 $O(nm)$** 。
数组 `dp` 大小为 $n \times m$ **因此空间复杂度为 $O(nm)$** 。
@@ -427,3 +445,9 @@ $$
```dart title="min_path_sum.dart"
[class]{}-[func]{minPathSumDPComp}
```
=== "Rust"
```rust title="min_path_sum.rs"
[class]{}-[func]{min_path_sum_dp_comp}
```

View File

@@ -131,6 +131,12 @@ $$
[class]{}-[func]{editDistanceDP}
```
=== "Rust"
```rust title="edit_distance.rs"
[class]{}-[func]{edit_distance_dp}
```
如下图所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作是填写一个二维网格的过程。
=== "<1>"
@@ -249,3 +255,9 @@ $$
```dart title="edit_distance.dart"
[class]{}-[func]{editDistanceDPComp}
```
=== "Rust"
```rust title="edit_distance.rs"
[class]{}-[func]{edit_distance_dp_comp}
```

View File

@@ -102,6 +102,14 @@
[class]{}-[func]{climbingStairsBacktrack}
```
=== "Rust"
```rust title="climbing_stairs_backtrack.rs"
[class]{}-[func]{backtrack}
[class]{}-[func]{climbing_stairs_backtrack}
```
## 方法一:暴力搜索
回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。
@@ -219,6 +227,14 @@ $$
[class]{}-[func]{climbingStairsDFS}
```
=== "Rust"
```rust title="climbing_stairs_dfs.rs"
[class]{}-[func]{dfs}
[class]{}-[func]{climbing_stairs_dfs}
```
下图展示了暴力搜索形成的递归树。对于问题 $dp[n]$ ,其递归树的深度为 $n$ ,时间复杂度为 $O(2^n)$ 。指数阶属于爆炸式增长,如果我们输入一个比较大的 $n$ ,则会陷入漫长的等待之中。
![爬楼梯对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png)
@@ -322,6 +338,14 @@ $$
[class]{}-[func]{climbingStairsDFSMem}
```
=== "Rust"
```rust title="climbing_stairs_dfs_mem.rs"
[class]{}-[func]{dfs}
[class]{}-[func]{climbing_stairs_dfs_mem}
```
观察下图,**经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 $O(n)$** ,这是一个巨大的飞跃。
![记忆化搜索对应递归树](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png)
@@ -400,6 +424,12 @@ $$
[class]{}-[func]{climbingStairsDP}
```
=== "Rust"
```rust title="climbing_stairs_dp.rs"
[class]{}-[func]{climbing_stairs_dp}
```
与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的某个特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 $i$ 。
总结以上,动态规划的常用术语包括:
@@ -480,6 +510,12 @@ $$
[class]{}-[func]{climbingStairsDPComp}
```
=== "Rust"
```rust title="climbing_stairs_dp.rs"
[class]{}-[func]{climbing_stairs_dp_comp}
```
观察以上代码,由于省去了数组 `dp` 占用的空间,因此空间复杂度从 $O(n)$ 降低至 $O(1)$ 。
**这种空间优化技巧被称为「状态压缩」**。在常见的动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以应用状态压缩,只保留必要的状态,通过“降维”来节省内存空间。

View File

@@ -122,6 +122,12 @@ $$
[class]{}-[func]{knapsackDFS}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dfs}
```
如下图所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 $O(2^n)$ 。
观察递归树,容易发现其中存在重叠子问题,例如 $dp[1, 10]$ 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。
@@ -200,6 +206,12 @@ $$
[class]{}-[func]{knapsackDFSMem}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dfs_mem}
```
![0-1 背包的记忆化搜索递归树](knapsack_problem.assets/knapsack_dfs_mem.png)
### 方法三:动态规划
@@ -272,6 +284,12 @@ $$
[class]{}-[func]{knapsackDP}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dp}
```
如下图所示,时间复杂度和空间复杂度都由数组 `dp` 大小决定,即 $O(n \times cap)$ 。
=== "<1>"
@@ -412,3 +430,9 @@ $$
```dart title="knapsack.dart"
[class]{}-[func]{knapsackDPComp}
```
=== "Rust"
```rust title="knapsack.rs"
[class]{}-[func]{knapsack_dp_comp}
```

View File

@@ -96,6 +96,12 @@ $$
[class]{}-[func]{unboundedKnapsackDP}
```
=== "Rust"
```rust title="unbounded_knapsack.rs"
[class]{}-[func]{unbounded_knapsack_dp}
```
### 状态压缩
由于当前状态是从左边和上边的状态转移而来,**因此状态压缩后应该对 $dp$ 表中的每一行采取正序遍历**。
@@ -188,6 +194,12 @@ $$
[class]{}-[func]{unboundedKnapsackDPComp}
```
=== "Rust"
```rust title="unbounded_knapsack.rs"
[class]{}-[func]{unbounded_knapsack_dp_comp}
```
## 零钱兑换问题
背包问题是一大类动态规划问题的代表,其拥有很多的变种,例如零钱兑换问题。
@@ -301,6 +313,12 @@ $$
[class]{}-[func]{coinChangeDP}
```
=== "Rust"
```rust title="coin_change.rs"
[class]{}-[func]{coin_change_dp}
```
下图展示了零钱兑换的动态规划过程,和完全背包非常相似。
=== "<1>"
@@ -418,6 +436,12 @@ $$
[class]{}-[func]{coinChangeDPComp}
```
=== "Rust"
```rust title="coin_change.rs"
[class]{}-[func]{coin_change_dp_comp}
```
## 零钱兑换问题 II
!!! question
@@ -504,6 +528,12 @@ $$
[class]{}-[func]{coinChangeIIDP}
```
=== "Rust"
```rust title="coin_change_ii.rs"
[class]{}-[func]{coin_change_ii_dp}
```
### 状态压缩
状态压缩处理方式相同,删除硬币维度即可。
@@ -573,3 +603,9 @@ $$
```dart title="coin_change_ii.dart"
[class]{}-[func]{coinChangeIIDPComp}
```
=== "Rust"
```rust title="coin_change_ii.rs"
[class]{}-[func]{coin_change_ii_dp_comp}
```