Finetune the articles.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
@@ -14,7 +14,7 @@
|
||||
|
||||
如下图所示,若第 $1$ , $2$ , $3$ 阶的代价分别为 $1$ , $10$ , $1$ ,则从地面爬到第 $3$ 阶的最小代价为 $2$ 。
|
||||
|
||||

|
||||

|
||||
|
||||
设 $dp[i]$ 为爬到第 $i$ 阶累计付出的代价,由于第 $i$ 阶只可能从 $i - 1$ 阶或 $i - 2$ 阶走来,因此 $dp[i]$ 只可能等于 $dp[i - 1] + cost[i]$ 或 $dp[i - 2] + cost[i]$ 。为了尽可能减少代价,我们应该选择两者中较小的那一个,即:
|
||||
|
||||
@@ -96,7 +96,7 @@ $$
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
这道题同样也可以进行状态压缩,将一维压缩至零维,使得空间复杂度从 $O(n)$ 降低至 $O(1)$ 。
|
||||
|
||||
@@ -180,7 +180,7 @@ $$
|
||||
|
||||
例如,爬上第 $3$ 阶仅剩 $2$ 种可行方案,其中连续三次跳 $1$ 阶的方案不满足约束条件,因此被舍弃。
|
||||
|
||||

|
||||

|
||||
|
||||
在该问题中,**下一步选择不能由当前状态(当前楼梯阶数)独立决定,还和前一个状态(上轮楼梯阶数)有关**。如果上一轮是跳 $1$ 阶上来的,那么下一轮就必须跳 $2$ 阶。
|
||||
|
||||
@@ -200,7 +200,7 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||

|
||||

|
||||
|
||||
最终,返回 $dp[n, 1] + dp[n, 2]$ 即可,两者之和代表爬到第 $n$ 阶的方案总数。
|
||||
|
||||
|
||||
@@ -85,14 +85,14 @@ $$
|
||||
|
||||
边界条件即初始状态,在搜索中用于剪枝,在动态规划中用于初始化 $dp$ 表。状态转移顺序的核心是要保证在计算当前问题时,所有它依赖的更小子问题都已经被正确地计算出来。
|
||||
|
||||
最后,我们基于以上结果实现解法即可。熟练度较高同学可以直接写出动态规划解法,初学者可以按照“暴力搜索 $\rightarrow$ 记忆化搜索 $\rightarrow$ 动态规划” 的顺序实现。
|
||||
接下来,我们就可以实现动态规划代码了。然而,由于子问题分解是一种从顶至底的思想,因此按照“暴力搜索 $\rightarrow$ 记忆化搜索 $\rightarrow$ 动态规划”的顺序实现更加符合思维习惯。
|
||||
|
||||
## 方法一:暴力搜索
|
||||
|
||||
从状态 $[i, j]$ 开始搜索,不断分解为更小的状态 $[i-1, j]$ 和 $[i, j-1]$ ,包括以下递归要素:
|
||||
|
||||
- **递归参数**:状态 $[i, j]$ ;**返回值**:从 $[0, 0]$ 到 $[i, j]$ 的最小路径和 $dp[i, j]$ ;
|
||||
- **终止条件**:当 $i = 0$ 且 $j = 0$ 时,返回代价 $grid[0][0]$ ;
|
||||
- **终止条件**:当 $i = 0$ 且 $j = 0$ 时,返回代价 $grid[0, 0]$ ;
|
||||
- **剪枝**:当 $i < 0$ 时或 $j < 0$ 时索引越界,此时返回代价 $+\infty$ ,代表不可行;
|
||||
|
||||
=== "Java"
|
||||
@@ -250,7 +250,7 @@ $$
|
||||
=== "Java"
|
||||
|
||||
```java title="min_path_sum.java"
|
||||
[class]{min}-[func]{minPathSumDP}
|
||||
[class]{min_path_sum}-[func]{minPathSumDP}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
@@ -358,7 +358,7 @@ $$
|
||||
=== "Java"
|
||||
|
||||
```java title="min_path_sum.java"
|
||||
[class]{min}-[func]{minPathSumDPComp}
|
||||
[class]{min_path_sum}-[func]{minPathSumDPComp}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
状态 $[i, c]$ 对应的子问题为:**前 $i$ 个物品在剩余容量为 $c$ 的背包中的最大价值**,记为 $dp[i, c]$ 。
|
||||
|
||||
至此,我们得到一个尺寸为 $n \times cap$ 的二维 $dp$ 矩阵。
|
||||
需要求解的是 $dp[n, cap]$ ,因此需要一个尺寸为 $(n+1) \times (cap+1)$ 的二维 $dp$ 表。
|
||||
|
||||
**第二步:找出最优子结构,进而推导出状态转移方程**
|
||||
|
||||
@@ -45,6 +45,10 @@ $$
|
||||
|
||||
当前状态 $[i, c]$ 从上方的状态 $[i-1, c]$ 和左上方的状态 $[i-1, c-wgt[i-1]]$ 转移而来,因此通过两层循环正序遍历整个 $dp$ 表即可。
|
||||
|
||||
!!! tip
|
||||
|
||||
完成以上三步后,我们可以直接实现从底至顶的动态规划解法。而为了展示本题包含的重叠子问题,本文也同时给出从顶至底的暴力搜索和记忆化搜索解法。
|
||||
|
||||
## 方法一:暴力搜索
|
||||
|
||||
搜索代码包含以下要素:
|
||||
@@ -201,7 +205,7 @@ $$
|
||||
|
||||
## 方法三:动态规划
|
||||
|
||||
动态规划解法本质上就是在状态转移中填充 `dp` 矩阵的过程,代码如下所示。
|
||||
动态规划解法本质上就是在状态转移中填充 $dp$ 表的过程,代码如下所示。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@@ -269,7 +273,7 @@ $$
|
||||
[class]{}-[func]{knapsackDP}
|
||||
```
|
||||
|
||||
如下图所示,时间复杂度由 `dp` 矩阵大小决定,为 $O(n \times cap)$ 。
|
||||
如下图所示,时间复杂度由数组 `dp` 大小决定,为 $O(n \times cap)$ 。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
@@ -313,9 +317,9 @@ $$
|
||||
=== "<14>"
|
||||

|
||||
|
||||
**最后考虑状态压缩**。以上代码中的 `dp` 矩阵占用 $O(n \times cap)$ 空间。由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 $O(n^2)$ 将低至 $O(n)$ 。代码省略,有兴趣的同学可以自行实现。
|
||||
**最后考虑状态压缩**。以上代码中的数组 `dp` 占用 $O(n \times cap)$ 空间。由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 $O(n^2)$ 将低至 $O(n)$ 。代码省略,有兴趣的同学可以自行实现。
|
||||
|
||||
那么,我们是否可以仅用一个数组实现状态压缩呢?观察可知,每个状态都是由左上方或正上方的格子转移过来的。假设只有一个数组,当遍历到第 $i$ 行时,该数组存储的仍然是第 $i-1$ 行的状态,为了避免左边区域的格子在状态转移中被覆盖,我们应采取倒序遍历。
|
||||
那么,我们是否可以仅用一个数组实现状态压缩呢?观察可知,每个状态都是由左上方或正上方的格子转移过来的。假设只有一个数组,当遍历到第 $i$ 行时,该数组存储的仍然是第 $i-1$ 行的状态,**为了避免左方区域的格子在状态转移中被覆盖,应该采取倒序遍历**。
|
||||
|
||||
以下动画展示了在单个数组下从第 $i=1$ 行转换至第 $i=2$ 行的过程。建议你思考一下正序遍历和倒序遍历的区别。
|
||||
|
||||
@@ -337,7 +341,7 @@ $$
|
||||
=== "<6>"
|
||||

|
||||
|
||||
如以下代码所示,我们仅需将 `dp` 矩阵的第一维 $i$ 直接删除,并且将内循环修改为倒序遍历即可。
|
||||
如以下代码所示,我们仅需将数组 `dp` 的第一维 $i$ 直接删除,并且将内循环修改为倒序遍历即可。
|
||||
|
||||
=== "Java"
|
||||
|
||||
|
||||