文章目录
- day38学习内容
- 一、动态规划理论基础
- 1.1、动态规划理论基础的几个关键概念:
- 1.2、动态规划五部曲
- 二、斐波那契数
- 2.1、动态规划五部曲
- 2.1.1、 确定dp数组(dp table)以及下标的含义
- 2.1.2、确定递推公式
- 2.1.3、 dp数组如何初始化
- 2.1.4、确定遍历顺序
- 2.1.5、计算并返回最终结果
- 2.2、代码
- 2.2.1、为什么dp[0] = 0?
- 三、爬楼梯
- 3.1、动态规划五部曲
- 3.1.1、 确定dp数组(dp table)以及下标的含义
- 3.1.2、确定递推公式
- 3.1.3、 dp数组如何初始化
- 3.1.4、确定遍历顺序
- 3.1.5、计算并返回最终结果
- 3.2、代码
- 四、最小花费爬楼梯
- 4.1、动态规划五部曲
- 4.1.1、 确定dp数组(dp table)以及下标的含义
- 4.1.2、确定递推公式
- 4.1.3、 dp数组如何初始化
- 4.1.4、确定遍历顺序
- 4.1.5、计算并返回最终结果
- 4.2、代码
- 总结
- 1.感想
- 2.思维导图
day38学习内容
day37主要内容
- 斐波那契数
- 爬楼梯
最小花费爬楼梯
声明
本文思路和文字,引用自《代码随想录》
一、动态规划理论基础
1.1、动态规划理论基础的几个关键概念:
-
最优子结构(Optimal Substructure):一个问题的最优解包含其子问题的最优解。这意味着我们可以通过组合子问题的最优解来构造问题的最优解。
-
边界条件(Boundary Conditions):问题的最小子集,它们是无法再分解的,并且解决方案是显而易见的。这些条件是动态规划算法的起点。
-
状态转移方程(State Transition Equation):这是动态规划的核心,定义了从一个状态到另一个状态的转移规则。一个“状态”通常描述了解决问题的某个阶段,而状态转移方程描述了这些阶段如何转化。
-
备忘录法(Memoization):一种优化技术,用于通过存储之前计算的结果(即“记忆”这些结果)来避免重复计算。这通常用于递归解决方案中,以提高效率。
-
自底向上(Bottom-Up Approach):与递归(通常是自顶向下的)相反,自底向上的方法首先解决子问题,然后合并这些解决方案来解决更大的问题。这通常涉及到使用迭代而非递归,并且使用表格(数组或其他数据结构)来存储中间结果。
1.2、动态规划五部曲
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
二、斐波那契数
509.原题链接
2.1、动态规划五部曲
2.1.1、 确定dp数组(dp table)以及下标的含义
- dp[i]表示第i个数,和为dp[i]
2.1.2、确定递推公式
题意已经告诉你了。dp[i] = dp[i - 1] + dp[i - 2];,不用自己推导了。
2.1.3、 dp数组如何初始化
很明显。dp[0] = 0
dp[1] = 1
2.1.4、确定遍历顺序
从前向后遍历,没啥好说的
2.1.5、计算并返回最终结果
return dp[i]即可
2.2、代码
class Solution {
public int fib(int n) {
if (n < 2) {
return n;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
2.2.1、为什么dp[0] = 0?
看题目。。
三、爬楼梯
70.原题链接
3.1、动态规划五部曲
3.1.1、 确定dp数组(dp table)以及下标的含义
- dp[i]表示到达第i个楼梯,有dp[i]种方法
3.1.2、确定递推公式
3.1.3、 dp数组如何初始化
很明显。dp[0] = 1
dp[1] = 1
3.1.4、确定遍历顺序
从前向后遍历,没啥好说的
3.1.5、计算并返回最终结果
return dp[i]即可
3.2、代码
class Solution {
public int climbStairs(int n) {
if (n < 2) {
return n;
}
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
四、最小花费爬楼梯
746.原题链接
4.1、动态规划五部曲
4.1.1、 确定dp数组(dp table)以及下标的含义
- dp[i] 表示到达第i层楼梯,最小成本为dp[i]。
4.1.2、确定递推公式
推导过程
-
定义状态:首先,我们定义
dp[i]
为到达第i
阶楼梯所需的最小成本。 -
分析选择:对于每一阶楼梯
i
(除了第一和第二阶),我们可以从第i-1
阶或第i-2
阶到达。这意味着到达第i
阶的最小成本取决于:- 从第
i-1
阶上来的成本(即dp[i-1]
加上cost[i]
),以及 - 从第
i-2
阶上来的成本(即dp[i-2]
加上cost[i]
)。
- 从第
-
确定状态转移方程:因此,为了计算
dp[i]
,我们需要选择这两种方式中成本更低的一种,即:
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
这里dp[i-1] + cost[i]
代表了通过先支付到达第i-1
阶的成本,再支付第i
阶的成本来到达第i
阶的总成本。类似地,dp[i-2] + cost[i]
代表了先支付到达第i-2
阶的成本,再支付第i
阶的成本的总成本。
4.1.3、 dp数组如何初始化
很明显。对于初始阶梯 dp[0]
和 dp[1]
,因为没有前面的阶梯可以选择,所以它们的成本直接等于 cost[0]
和 cost[1]
。
4.1.4、确定遍历顺序
从前向后遍历,没啥好说的
4.1.5、计算并返回最终结果
返回达到倒数第一阶和倒数第二阶中最小成本的那一个,因为这两个位置都可以直接到达楼梯顶端。
return Math.min(dp[cost.length - 1], dp[cost.length - 2]);
return Math.min(dp[cost.length - 1], dp[cost.length - 2])为什么不用加cost[i]
因为:
-
问题定义:数组
cost
中的每个元素代表爬上对应阶梯的成本。一旦支付了爬上某阶梯的成本,你就可以站在那一阶上。而目标是以最小的成本到达楼梯的顶部,顶部位于cost
数组之外的位置。 -
最终目标:考虑到你可以一次爬一阶或两阶,到达顶部的最后一步可以从倒数第一阶开始,也可以从倒数第二阶开始。这意味着在实际达到楼梯顶部时,并不需要支付额外的成本,因为你已经站在了可以直接到达顶部的阶梯上。
-
成本已包括:当计算
dp
数组时,每个dp[i]
已经包含了到达第i
阶所需的全部成本,包括爬上第i
阶的成本。所以,到达倒数第一阶或倒数第二阶的成本已经计算在内,不需要再额外添加成本来完成最后一步的动作。
综上所述,选择 dp[cost.length - 1]
和 dp[cost.length - 2]
中的最小值作为最终结果,是因为这两个值已经反映了到达楼梯顶部(实际是站在能够一步到达顶部的最后两阶楼梯上)的全部必要成本。这个设计巧妙地避免了在问题解决方案的最后一步中不必要的成本计算,直接利用了动态规划数组中的结果来找出达到楼梯顶部的最小成本。
4.2、代码
class Solution {
public int minCostClimbingStairs(int[] cost) {
int[] dp = new int[cost.length];
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i < cost.length; i++) {
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
}
return Math.min(dp[cost.length - 1], dp[cost.length - 2]);
}
}
总结
1.感想
- 动态规划的第一天,加油
2.思维导图
本文思路引用自代码随想录,感谢代码随想录作者。-