mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-10 06:10:19 +08:00
Mention figures and tables in normal texts.
Fix some figures. Finetune texts.
This commit is contained in:
@@ -32,7 +32,7 @@ $$
|
||||
|
||||
那么,上节的爬楼梯题目有没有最优子结构呢?它的目标是求解方案数量,看似是一个计数问题,但如果换一种问法:“求解最大方案数量”。我们意外地发现,**虽然题目修改前后是等价的,但最优子结构浮现出来了**:第 $n$ 阶最大方案数量等于第 $n-1$ 阶和第 $n-2$ 阶最大方案数量之和。所以说,最优子结构的解释方式比较灵活,在不同问题中会有不同的含义。
|
||||
|
||||
根据状态转移方程,以及初始状态 $dp[1] = cost[1]$ , $dp[2] = cost[2]$ ,可以得出动态规划代码。
|
||||
根据状态转移方程,以及初始状态 $dp[1] = cost[1]$ , $dp[2] = cost[2]$ ,我们就可以得到动态规划代码。
|
||||
|
||||
=== "Java"
|
||||
|
||||
@@ -106,6 +106,8 @@ $$
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp}
|
||||
```
|
||||
|
||||
下图展示了以上代码的动态规划过程。
|
||||
|
||||

|
||||
|
||||
本题也可以进行状态压缩,将一维压缩至零维,使得空间复杂度从 $O(n)$ 降低至 $O(1)$ 。
|
||||
@@ -194,7 +196,7 @@ $$
|
||||
|
||||
给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,**但不能连续两轮跳 $1$ 阶**,请问有多少种方案可以爬到楼顶。
|
||||
|
||||
例如,爬上第 $3$ 阶仅剩 $2$ 种可行方案,其中连续三次跳 $1$ 阶的方案不满足约束条件,因此被舍弃。
|
||||
例如下图,爬上第 $3$ 阶仅剩 $2$ 种可行方案,其中连续三次跳 $1$ 阶的方案不满足约束条件,因此被舍弃。
|
||||
|
||||

|
||||
|
||||
@@ -207,7 +209,7 @@ $$
|
||||
- 当 $j$ 等于 $1$ ,即上一轮跳了 $1$ 阶时,这一轮只能选择跳 $2$ 阶。
|
||||
- 当 $j$ 等于 $2$ ,即上一轮跳了 $2$ 阶时,这一轮可选择跳 $1$ 阶或跳 $2$ 阶。
|
||||
|
||||
在该定义下,$dp[i, j]$ 表示状态 $[i, j]$ 对应的方案数。在该定义下的状态转移方程为:
|
||||
如下图所示,在该定义下,$dp[i, j]$ 表示状态 $[i, j]$ 对应的方案数。此时状态转移方程为:
|
||||
|
||||
$$
|
||||
\begin{cases}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
给定一个 $n \times m$ 的二维网格 `grid` ,网格中的每个单元格包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回从左上角到右下角的最小路径和。
|
||||
|
||||
例如以下示例数据,给定网格的最小路径和为 $13$ 。
|
||||
下图展示了一个例子,给定网格的最小路径和为 $13$ 。
|
||||
|
||||

|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
状态 $[i, j]$ 对应的子问题为:从起始点 $[0, 0]$ 走到 $[i, j]$ 的最小路径和,解记为 $dp[i, j]$ 。
|
||||
|
||||
至此,我们就得到了一个二维 $dp$ 矩阵,其尺寸与输入网格 $grid$ 相同。
|
||||
至此,我们就得到了下图所示的二维 $dp$ 矩阵,其尺寸与输入网格 $grid$ 相同。
|
||||
|
||||

|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
对于状态 $[i, j]$ ,它只能从上边格子 $[i-1, j]$ 和左边格子 $[i, j-1]$ 转移而来。因此最优子结构为:到达 $[i, j]$ 的最小路径和由 $[i, j-1]$ 的最小路径和与 $[i-1, j]$ 的最小路径和,这两者较小的那一个决定。
|
||||
|
||||
根据以上分析,可推出以下状态转移方程:
|
||||
根据以上分析,可推出下图所示的状态转移方程:
|
||||
|
||||
$$
|
||||
dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
@@ -77,7 +77,7 @@ $$
|
||||
|
||||
在本题中,处在首行的状态只能向右转移,首列状态只能向下转移,因此首行 $i = 0$ 和首列 $j = 0$ 是边界条件。
|
||||
|
||||
每个格子是由其左方格子和上方格子转移而来,因此我们使用采用循环来遍历矩阵,外循环遍历各行、内循环遍历各列。
|
||||
如下图所示,由于每个格子是由其左方格子和上方格子转移而来,因此我们使用采用循环来遍历矩阵,外循环遍历各行、内循环遍历各列。
|
||||
|
||||

|
||||
|
||||
@@ -254,7 +254,7 @@ $$
|
||||
[class]{}-[func]{min_path_sum_dfs_mem}
|
||||
```
|
||||
|
||||
引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 $O(nm)$ 。
|
||||
如下图所示,在引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 $O(nm)$ 。
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
**第二步:找出最优子结构,进而推导出状态转移方程**
|
||||
|
||||
考虑子问题 $dp[i, j]$ ,其对应的两个字符串的尾部字符为 $s[i-1]$ 和 $t[j-1]$ ,可根据不同编辑操作分为三种情况:
|
||||
考虑子问题 $dp[i, j]$ ,其对应的两个字符串的尾部字符为 $s[i-1]$ 和 $t[j-1]$ ,可根据不同编辑操作分为下图所示的三种情况:
|
||||
|
||||
1. 在 $s[i-1]$ 之后添加 $t[j-1]$ ,则剩余子问题 $dp[i, j-1]$ 。
|
||||
2. 删除 $s[i-1]$ ,则剩余子问题 $dp[i-1, j]$ 。
|
||||
|
||||
@@ -128,7 +128,7 @@ $$
|
||||
dp[i] = dp[i-1] + dp[i-2]
|
||||
$$
|
||||
|
||||
这意味着在爬楼梯问题中,各个子问题之间存在递推关系,**原问题的解可以由子问题的解构建得来**。
|
||||
这意味着在爬楼梯问题中,各个子问题之间存在递推关系,**原问题的解可以由子问题的解构建得来**。下图展示了该递推关系。
|
||||
|
||||

|
||||
|
||||
@@ -430,6 +430,10 @@ $$
|
||||
[class]{}-[func]{climbing_stairs_dp}
|
||||
```
|
||||
|
||||
下图模拟了以上代码的执行过程。
|
||||
|
||||

|
||||
|
||||
与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的某个特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 $i$ 。
|
||||
|
||||
总结以上,动态规划的常用术语包括:
|
||||
@@ -438,8 +442,6 @@ $$
|
||||
- 将最小子问题对应的状态(即第 $1$ , $2$ 阶楼梯)称为「初始状态」。
|
||||
- 将递推公式 $dp[i] = dp[i-1] + dp[i-2]$ 称为「状态转移方程」。
|
||||
|
||||

|
||||
|
||||
## 状态压缩
|
||||
|
||||
细心的你可能发现,**由于 $dp[i]$ 只与 $dp[i-1]$ 和 $dp[i-2]$ 有关,因此我们无须使用一个数组 `dp` 来存储所有子问题的解**,而只需两个变量滚动前进即可。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$ 、价值为 $val[i-1]$ ,和一个容量为 $cap$ 的背包。每个物品只能选择一次,问在不超过背包容量下能放入物品的最大价值。
|
||||
|
||||
请注意,物品编号 $i$ 从 $1$ 开始计数,数组索引从 $0$ 开始计数,因此物品 $i$ 对应重量 $wgt[i-1]$ 和价值 $val[i-1]$ 。
|
||||
观察下图,由于物品编号 $i$ 从 $1$ 开始计数,数组索引从 $0$ 开始计数,因此物品 $i$ 对应重量 $wgt[i-1]$ 和价值 $val[i-1]$ 。
|
||||
|
||||

|
||||
|
||||
@@ -212,6 +212,8 @@ $$
|
||||
[class]{}-[func]{knapsack_dfs_mem}
|
||||
```
|
||||
|
||||
下图展示了在记忆化递归中被剪掉的搜索分支。
|
||||
|
||||

|
||||
|
||||
### 方法三:动态规划
|
||||
@@ -343,7 +345,7 @@ $$
|
||||
- 如果采取正序遍历,那么遍历到 $dp[i, j]$ 时,左上方 $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ 值可能已经被覆盖,此时就无法得到正确的状态转移结果。
|
||||
- 如果采取倒序遍历,则不会发生覆盖问题,状态转移可以正确进行。
|
||||
|
||||
以下动画展示了在单个数组下从第 $i = 1$ 行转换至第 $i = 2$ 行的过程。请思考正序遍历和倒序遍历的区别。
|
||||
下图展示了在单个数组下从第 $i = 1$ 行转换至第 $i = 2$ 行的过程。请思考正序遍历和倒序遍历的区别。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
@@ -108,7 +108,7 @@ $$
|
||||
|
||||
由于当前状态是从左边和上边的状态转移而来,**因此状态压缩后应该对 $dp$ 表中的每一行采取正序遍历**。
|
||||
|
||||
这个遍历顺序与 0-1 背包正好相反。请通过以下动画来理解两者的区别。
|
||||
这个遍历顺序与 0-1 背包正好相反。请借助下图来理解两者的区别。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
Reference in New Issue
Block a user