1 今日打卡
斐波那契数 509. 斐波那契数 - 力扣(LeetCode)
使用最小花费爬楼梯 746. 使用最小花费爬楼梯 - 力扣(LeetCode)
2 dp基础理论
dp五部曲:
确定dp数组(dp table)以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
举例推导dp数组
3 斐波那契数
3.1 思路
第一步:
dp[i] 表示斐波那契数列的第 i 项的值;
下标i 对应斐波那契数列的项数(从 0 开始),比如dp[0]是第 0 项,dp[1]是第 1 项,dp[2]是第 2 项,以此类推。
第二步:
标准形式:dp[i] = dp[i-1] + dp[i-2](i ≥ 2),入门题目。因为直接给出了转移方程
第三步:
第 0 项:dp[0] = 0;
第 1 项:dp[1] = 1
第四步:
状态转移方程中,dp[i]依赖dp[i-1]和dp[i-2](前两项),因此必须从前往后遍历(从 2 到 n),才能保证计算当前项时,前两项已经有正确的值。
第五步:
以n=5为例,手动推导代码的执行过程,验证逻辑正确性:
初始状态:dp[0] = 0,dp[1] = 1(对应第 0、1 项);
i=2:
sum = 0+1 = 1 → dp [0] = 1(原 dp [1]),dp [1] = 1(第 2 项);
i=3:
sum = 1+1 = 2 → dp [0] = 1(原 dp [1]),dp [1] = 2(第 3 项);
i=4:
sum = 1+2 = 3 → dp [0] = 2(原 dp [1]),dp [1] = 3(第 4 项);
i=5:
sum = 2+3 = 5 → dp [0] = 3(原 dp [1]),dp [1] = 5(第 5 项);
最终返回dp[1] = 5,符合斐波那契数列第 5 项的结果(0,1,1,2,3,5)。
3.2 实现代码
java
class Solution {
public int fib(int n) {
int[] dp = new int[2];
dp[0] = 0;
dp[1] = 1;
if(n < 2) return dp[n];
for(int i = 2; i <= n; i++) {
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
}
4 爬楼梯
4.1 思路
第一步:
dp[i] 表示:爬到第 i 级台阶时,一共有多少种不同的方法;
第二步:
要爬到第i级台阶,最后一步只有两种可能:
从第i-1级台阶爬 1 级上来;
从第i-2级台阶爬 2 级上来。
状态转移方程:dp[i] = dp[i-1] + dp[i-2](i ≥ 2),即爬到第i级的方法数 = 爬到i-1级的方法数 + 爬到i-2级的方法数。
第三步:
dp[0] = 1:爬到第 0 级台阶(地面)只有 1 种方法(原地不动),这是为了让状态转移方程在i=2时成立(dp[2] = dp[1] + dp[0] = 1 + 1 = 2,符合实际:爬 2 级台阶有 "1+1""2" 两种方法);
dp[1] = 1:爬到第 1 级台阶只有 1 种方法(直接爬 1 级);
第四步:
状态转移方程中,dp[i]依赖dp[i-1]和dp[i-2](前两级的结果),因此必须从前往后遍历(从 2 到 n),保证计算dp[i]时,dp[i-1]和dp[i-2]已经有正确的值。
第五步:
以n=5为例,手动推导验证逻辑正确性:
初始状态:dp[0] = 1,dp[1] = 1;
i=2:dp[2] = dp[1] + dp[0] = 1 + 1 = 2(方法:1+1、2);
i=3:dp[3] = dp[2] + dp[1] = 2 + 1 = 3(方法:1+1+1、1+2、2+1);
i=4:dp[4] = dp[3] + dp[2] = 3 + 2 = 5;
i=5:dp[5] = dp[4] + dp[3] = 5 + 3 = 8;
最终dp[5] = 8,即爬 5 级台阶有 8 种不同方法,符合实际逻辑。
4.2 实现代码
java
class Solution {
public int climbStairs(int n) {
// 1. 定义dp数组:dp[i]表示爬到第i级台阶的方法数
// 数组长度为n+1,因为要覆盖0到n级台阶
int[] dp = new int[n + 1];
// 2. 初始化dp数组
dp[0] = 1; // 爬到0级(地面)只有1种方法(原地不动)
dp[1] = 1; // 爬到1级台阶只有1种方法(直接爬1级)
// 边界处理:n=1时直接返回结果,无需后续计算
if(n == 1) return 1;
// 3. 确定遍历顺序:从2到n正向遍历,因为dp[i]依赖dp[i-1]和dp[i-2]
for(int i = 2; i <= n; i++) {
// 4. 状态转移方程:爬到i级的方法数 = 爬到i-1级 + 爬到i-2级的方法数
dp[i] = dp[i-2] + dp[i-1];
}
// 5. 返回爬到第n级台阶的方法数
return dp[n];
}
}
5 使用最小花费爬楼梯
5.1 思路
第一步:
dp [i] 及其下标 i 的含义
dp[i] 表示:到达第 i 级台阶(准备向上爬)时,所需要花费的最小总代价;
第二步:
到达第i级台阶有两种途径:
从第i-1级台阶爬 1 级上来:总代价 = 到达i-1级的最小代价 + 爬i-1级的代价 → dp[i-1] + cost[i-1];
从第i-2级台阶爬 2 级上来:总代价 = 到达i-2级的最小代价 + 爬i-2级的代价 → dp[i-2] + cost[i-2];
状态转移方程:dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])(i ≥ 2),即取两种途径中代价更小的那个。
第三步:
dp[0] = 0:到达第 0 级台阶(起点),不需要花费任何代价;
dp[1] = 0:到达第 1 级台阶(起点),也不需要花费任何代价;
这两个初始化值是题目规则的直接体现,也是后续计算的基础。
第四步:
状态转移方程中,dp[i]依赖dp[i-1]和dp[i-2](前两级的最小代价),因此必须从前往后遍历(从 2 到len),保证计算dp[i]时,前两级的结果已经是确定的最小值。代码中的遍历逻辑:for(int i = 2; i <= len; i++),从第 2 级台阶开始,一直计算到楼顶(len级)。
第五步:
以cost = [10, 15, 20]为例(台阶数len=3,目标爬到第 3 级即楼顶),手动推导验证:
初始状态:dp[0] = 0,dp[1] = 0;
i=2(到达第 2 级台阶):
dp[2] = min(dp[1]+cost[1], dp[0]+cost[0]) = min(0+15, 0+10) = 10;
i=3(到达楼顶):
dp[3] = min(dp[2]+cost[2], dp[1]+cost[1]) = min(10+20, 0+15) = 15;
最终返回dp[3] = 15,即最小代价为 15(路径:从第 1 级台阶直接爬 2 级到楼顶,花费 15),符合实际逻辑。
5.2 实现代码
java
class Solution {
public int minCostClimbingStairs(int[] cost) {
// 获取台阶的总数(cost数组长度 = 台阶数)
int len = cost.length;
// 1. 定义dp数组:dp[i]表示到达第i级台阶时的最小总代价
// 数组长度为len+1,因为要计算到"楼顶"(第len级)的代价
int[] dp = new int[len + 1];
// 2. 初始化dp数组:从0级或1级台阶出发无需花费代价
dp[0] = 0; // 到达0级台阶(起点)的最小代价为0
dp[1] = 0; // 到达1级台阶(起点)的最小代价为0
// 3. 确定遍历顺序:从2到len正向遍历,依赖前两级的最小代价
for(int i = 2; i <= len; i++) {
// 4. 状态转移方程:
// 方式1:从i-1级爬1级上来,代价=dp[i-1]+cost[i-1]
// 方式2:从i-2级爬2级上来,代价=dp[i-2]+cost[i-2]
// 取两种方式中代价更小的作为dp[i]
dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
}
// 5. 返回到达楼顶(第len级)的最小总代价
return dp[len];
}
}