本文为力扣70:爬楼梯的详细解析。
虽然这道题的标签是“简单”,但是只有简单的题才能让我们专注于这类题的解题框架上。
一般来说动态规划会有三种解法:暴力解法、使用了备忘录自上而下的递归解法、使用了数组的自下而上的迭代解法。接下来我会对这三种解法逐一演示
根据上文 动态规划篇-00:解题思想与框架 首先我们要明确 [状态转移方程],这样我们就能写出最基础的暴力解法了。
思考状态转移方程的思路:base case → 明确状态 → 明确路径 → 定义dp函数
在此题中,最小子问题或者说是边界条件就是 “楼梯阶梯数为1或者2的时候”。这个边界问题是根据题意 “你每次可以爬1或2个台阶” 得来的
“原问题和子问题中会变化的变量”
此处的 “选择” 为 爬到楼顶的方法
“导致状态变化的行为”
此处 “状态” 为楼梯的阶数。正是因为楼梯阶数的变化,导致“爬到楼顶的方法”产生了变化。
根据题意,自然而然定义 dp(n) 为 “爬上n阶台阶的方法”
我们回想上一篇文章,“[分解问题]思路扩展一下就是动态规划算法”。那么dp(n)的子问题是什么?
我们最后一步是爬上第n阶阶梯,这一步之前我们需要决定是爬1步到n层(dp(n-1))还是爬两步到n层(dp(n-2))。
于是我们根据子问题和上述分析得出了状态转移方程:
状态转移方程中的 “状态转移” 在此处的表现就是:f(n)这个状态可以由 f(n-1) 和 f(n-2) 转移而来
有了状态转移方程就能写出暴力解法了
class Solution {
public int climbStairs(int n) {
return dp(n);
}
//定义一个方法dp,表示:有多少种方法爬上n阶阶梯
public int dp(int n){
//明确base case
if(n == 1 || n == 0){
return 1;
}
//写出状态转移方程
return dp(n-1) + dp(n-2);
}
}
但是这种解法是通过递归所有可能的情况来得出最终解,时间复杂度较高。这是因为存在大量“重叠子问题”。
比如想要计算 dp(6) ,就要计算dp(5) 和 dp(4)。计算dp(5) 又要计算dp(4) 和 dp(3)。但是dp(4) 已经在计算dp(6) 中计算过了。如果我们用一个备忘录把计算过的结果记录下来,需要的时候直接提取而不是重新计算,那么时间复杂度就会降下来。
class Solution {
public int climbStairs(int n) {
//定义一个备忘录数组用于记录数据
int[] memo = new int[n+1];
return dp(n,memo);
}
//定义一个方法dp,表示:有多少种方法爬上n阶阶梯
public int dp(int n,int[] memo){
//先将备忘录初始化为 -1
Arrays.fill(memo,-1);
//base case
if(n == 1 || n == 0){
return 1;
}
if(memo[n] != -1){
return memo[n];
}
//写出状态转移方程,并更新备忘录
memo[n] = dp(n-1,memo) + dp(n-2,memo);
return memo[n];
}
}
在这种方法中我们使用了一个数组 memo 来记录结果。例如 memo[5] 表示爬上5阶台阶的爬法,对应 dp(5) 的结果。
但是这里有两个问题需要注意:memo数组的长度应该是多少?memo 数组的初始化应该是多少?
我们设定memo数组的长度为 n+1,是为了让memo[n] 对应 dp[n] 的结果,如果memo数组的长度为n的话,memo[n-1] 对应dp(n),dp(0)就只能对应 memo[-1]了。
数组的索引怎么能是 -1 呢?
初始化值应该设定为一个 dp 函数无法取到的值。
如果把memo数组中的元素初始化为1。dp(1) = 1,memo[1] = 1。那么我怎么直到这个返回值是dp数组的返回值,还是memo数组初始化的返回值呢?
所以说,初始化值应该设定为一个 dp 函数无法取到的值。
我们定义一个dp数组,数组元素即为结果。比如dp[n] 定义为 : 爬上n阶阶梯的方法
class Solution {
public int climbStairs(int n) {
/*dp[i] 表示的是登录 i层阶梯的方法数量*/
int[] dp = new int[n+1];
if(n <= 2){
return n;
}
//base case
dp[0] = 0;dp[1] = 1;dp[2] = 2;
for(int i = 3;i <= n;i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
两者的定义起始并无区别。
dp(n) 的定义为 “爬上n阶阶梯的爬法”,dp[n]也是一样的。
dp(n) 是递归解法。比如想要算出dp(4),要算出dp(3) 和dp(2),依次往下,层层递进。直到递进到base case后,拿到数据,在层层回归,得到dp(4)。
而dp[n] 的迭代解法是自下而上的。逐步算出dp[1]、dp[2]、dp[3].....直到算出dp[n]。
那么对于其他题也是如此吗?我们来试试用这个方法算下一道题。