动态规划篇-01:爬楼梯

发布时间:2024年01月14日

本文为力扣70:爬楼梯的详细解析。

虽然这道题的标签是“简单”,但是只有简单的题才能让我们专注于这类题的解题框架上。

一般来说动态规划会有三种解法:暴力解法、使用了备忘录自上而下的递归解法、使用了数组的自下而上的迭代解法。接下来我会对这三种解法逐一演示

70:爬楼梯

根据上文?动态规划篇-00:解题思想与框架?首先我们要明确 [状态转移方程],这样我们就能写出最基础的暴力解法了。

状态转移方程

思考状态转移方程的思路:base case → 明确状态?→ 明确路径?→ 定义dp函数

base case

在此题中,最小子问题或者说是边界条件就是 “楼梯阶梯数为1或者2的时候”。这个边界问题是根据题意 “你每次可以爬1或2个台阶” 得来的

明确状态

“原问题和子问题中会变化的变量”

此处的 “选择” 为 爬到楼顶的方法

确定选择

“导致状态变化的行为”

此处 “状态” 为楼梯的阶数。正是因为楼梯阶数的变化,导致“爬到楼顶的方法”产生了变化。

定义dp函数

根据题意,自然而然定义 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 数组的初始化应该是多少?

问题1:?memo数组的长度应该是多少?

我们设定memo数组的长度为 n+1,是为了让memo[n] 对应 dp[n] 的结果,如果memo数组的长度为n的话,memo[n-1] 对应dp(n),dp(0)就只能对应 memo[-1]了。

数组的索引怎么能是 -1 呢?

问题2:?memo 数组的初始化应该是多少?

初始化值应该设定为一个 dp 函数无法取到的值。

如果把memo数组中的元素初始化为1。dp(1) = 1,memo[1] = 1。那么我怎么直到这个返回值是dp数组的返回值,还是memo数组初始化的返回值呢?

所以说,初始化值应该设定为一个 dp 函数无法取到的值。

使用了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];
    }
}

问题1:迭代解法和递归解法的区别

两者的定义起始并无区别。

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]。


那么对于其他题也是如此吗?我们来试试用这个方法算下一道题。

文章来源:https://blog.csdn.net/From_C/article/details/135579354
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。