一篇带你了解DP入门之爬楼梯

时间:2022-11-30 21:37:01

一篇带你了解DP入门之爬楼梯

爬楼梯

力扣题目链接:https://leetcode-cn.com/problems/climbing-stairs

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

  1. 输入:2
  2. 输出:2
  3. 解释:有两种方法可以爬到楼顶。
  • 1 阶 + 1 阶
  • 2 阶

示例 2:

  1. 输入:3
  2. 输出:3
  3. 解释:有三种方法可以爬到楼顶。
  • 1 阶 + 1 阶 + 1 阶
  • 1 阶 + 2 阶
  • 2 阶 + 1 阶

思路

本题大家如果没有接触过的话,会感觉比较难,多举几个例子,就可以发现其规律。

爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。

那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。

所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。

我们来分析一下,动规五部曲:

定义一个一维数组来记录不同楼层的状态

确定dp数组以及下标的含义

dp[i]:爬到第i层楼梯,有dp[i]种方法

确定递推公式

如果可以推出dp[i]呢?

从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。

首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。

还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。

那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!

所以dp[i] = dp[i - 1] + dp[i - 2] 。

在推导dp[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。

这体现出确定dp数组以及下标的含义的重要性!

dp数组如何初始化

在回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]中方法。

那么i为0,dp[i]应该是多少呢,这个可以有很多解释,但都基本是直接奔着答案去解释的。

例如强行安慰自己爬到第0层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶。

但总有点牵强的成分。

那还这么理解呢:我就认为跑到第0层,方法就是0啊,一步只能走一个台阶或者两个台阶,然而楼层是0,直接站楼顶上了,就是不用方法,dp[0]就应该是0.

其实这么争论下去没有意义,大部分解释说dp[0]应该为1的理由其实是因为dp[0]=1的话在递推的过程中i从2开始遍历本题就能过,然后就往结果上靠去解释dp[0] = 1。

从dp数组定义的角度上来说,dp[0] = 0 也能说得通。

需要注意的是:题目中说了n是一个正整数,题目根本就没说n有为0的情况。

所以本题其实就不应该讨论dp[0]的初始化!

我相信dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。

所以我的原则是:不考虑dp[0]如果初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。

确定遍历顺序

从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的

举例推导dp数组

举例当n为5的时候,dp table(dp数组)应该是这样的

一篇带你了解DP入门之爬楼梯

爬楼梯

如果代码出问题了,就把dp table 打印出来,看看究竟是不是和自己推导的一样。

此时大家应该发现了,这不就是斐波那契数列么!

唯一的区别是,没有讨论dp[0]应该是什么,因为dp[0]在本题没有意义!

以上五部分析完之后,C++代码如下:

  1. // 版本一
  2. class Solution {
  3. public:
  4. int climbStairs(int n) {
  5. if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针
  6. vector<int> dp(n + 1);
  7. dp[1] = 1;
  8. dp[2] = 2;
  9. for (int i = 3; i <= n; i++) { // 注意i是从3开始的
  10. dp[i] = dp[i - 1] + dp[i - 2];
  11. }
  12. return dp[n];
  13. }
  14. };
  • 时间复杂度:
  • 空间复杂度:

当然依然也可以,优化一下空间复杂度,代码如下:

  1. // 版本二
  2. class Solution {
  3. public:
  4. int climbStairs(int n) {
  5. if (n <= 1) return n;
  6. int dp[3];
  7. dp[1] = 1;
  8. dp[2] = 2;
  9. for (int i = 3; i <= n; i++) {
  10. int sum = dp[1] + dp[2];
  11. dp[1] = dp[2];
  12. dp[2] = sum;
  13. }
  14. return dp[2];
  15. }
  16. };
  • 时间复杂度:
  • 空间复杂度:

后面将讲解的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,但我个人认为面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化。

因为版本一才能体现出动规的思想精髓,递推的状态变化。

拓展

这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。

这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,所以后续我在讲解背包问题的时候,今天这道题还会拿从背包问题的角度上来再讲一遍。

这里我先给出我的实现代码:

  1. class Solution {
  2. public:
  3. int climbStairs(int n) {
  4. vector<int> dp(n + 1, 0);
  5. dp[0] = 1;
  6. for (int i = 1; i <= n; i++) {
  7. for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题
  8. if (i - j >= 0) dp[i] += dp[i - j];
  9. }
  10. }
  11. return dp[n];
  12. }
  13. };

代码中m表示最多可以爬m个台阶。

以上代码不能运行哈,我主要是为了体现只要把m换成2,粘过去,就可以AC爬楼梯这道题,不信你就粘一下试试,哈哈。

此时我就发现一个绝佳的大厂面试题,第一道题就是单纯的爬楼梯,然后看候选人的代码实现,如果把dp[0]的定义成1了,就可以发难了,为什么dp[0]一定要初始化为1,此时可能候选人就要强行给dp[0]应该是1找各种理由。那这就是一个考察点了,对dp[i]的定义理解的不深入。

然后可以继续发难,如果一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。这道题目leetcode上并没有原题,绝对是考察候选人算法能力的绝佳好题。

这一连套问下来,候选人算法能力如何,面试官心里就有数了。

其实大厂面试最喜欢问题的就是这种简单题,然后慢慢变化,在小细节上考察候选人。

总结

这道题目和动态规划:斐波那契数题目基本是一样的,但是会发现本题相比动态规划:斐波那契数难多了,为什么呢?

关键是 动态规划:斐波那契数 题目描述就已经把动规五部曲里的递归公式和如何初始化都给出来了,剩下几部曲也自然而然的推出来了。

而本题,就需要逐个分析了,大家现在应该初步感受出关于动态规划,你该了解这些!里给出的动规五部曲了。

简单题是用来掌握方法论的,例如昨天斐波那契的题目够简单了吧,但昨天和今天可以使用一套方法分析出来的,这就是方法论!

所以不要轻视简单题,那种凭感觉就刷过去了,其实和没掌握区别不大,只有掌握方法论并说清一二三,才能触类旁通,举一反三哈!

就酱,循序渐进学算法,认准「代码随想录」!

其他语言版本

Java

  1. class Solution {
  2. public int climbStairs(int n) {
  3. // 跟斐波那契数列一样
  4. if(n <= 2) return n;
  5. int a = 1, b = 2, sum = 0;
  6. for(int i = 3; i <= n; i++){
  7. sum = a + b;
  8. a = b;
  9. b = sum;
  10. }
  11. return b;
  12. }
  13. }
  1. // 常规方式
  2. public int climbStairs(int n) {
  3. int[] dp = new int[n + 1];
  4. dp[0] = 1;
  5. dp[1] = 1;
  6. for (int i = 2; i <= n; i++) {
  7. dp[i] = dp[i - 1] + dp[i - 2];
  8. }
  9. return dp[n];
  10. }
  11. // 用变量记录代替数组
  12. public int climbStairs(int n) {
  13. int a = 0, b = 1, c = 0; // 默认需要1次
  14. for (int i = 1; i <= n; i++) {
  15. c = a + b; // f(i - 1) + f(n - 2)
  16. a = b; // 记录上一轮的值
  17. b = c; // 向后步进1个数
  18. }
  19. return c;
  20. }

Python

  1. class Solution:
  2. def climbStairs(self, n: int) -> int:
  3. # dp[i]表示爬到第i级楼梯的种数, (1, 2) (2, 1)是两种不同的类型
  4. dp = [0] * (n + 1)
  5. dp[0] = 1
  6. for i in range(n+1):
  7. for j in range(1, 3):
  8. if i>=j:
  9. dp[i] += dp[i-j]
  10. return dp[-1]

Go

  1. func climbStairs(n int) int {
  2. if n==1{
  3. return 1
  4. }
  5. dp:=make([]int,n+1)
  6. dp[1]=1
  7. dp[2]=2
  8. for i:=3;i<=n;i++{
  9. dp[i]=dp[i-1]+dp[i-2]
  10. }
  11. return dp[n]
  12. }

Javascript

  1. var climbStairs = function(n) {
  2. // dp[i] 为第 i 阶楼梯有多少种方法爬到楼顶
  3. // dp[i] = dp[i - 1] + dp[i - 2]
  4. let dp = [1 , 2]
  5. for(let i = 2; i < n; i++) {
  6. dp[i] = dp[i - 1] + dp[i - 2]
  7. }
  8. return dp[n - 1]
  9. };

原文链接:https://mp.weixin.qq.com/s/l-BAAVDWLIfNs1sz6GVBlA