【算法-LeetCode】70. 爬楼梯(动态规划入门)

LeetCode70. 爬楼梯

发布:2021年7月28日23:41:15

问题描述及示例

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

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

示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
【1】1 阶 + 1 阶
【2】 2 阶

示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
【1】 1 阶 + 1 阶 + 1 阶
【2】1 阶 + 2 阶
【3】2 阶 + 1 阶

我的题解(动态规划)

更新:2021年8月12日15:52:06

这道题其实就是包装过的斐波那契数列问题,可以看下方的参考链接进行预热:

参考:【算法-剑指 Offer】10- I. 斐波那契数列_赖念安的博客-CSDN博客

【更新结束】

更新:2021年7月29日09:55:53

又是一道动态规划的题目,这算是我在LeetCode上遇到的第二道了。不过这道比我之前做的那道(详情请参照:【算法-LeetCode】53. 最大子序和_赖念安的博客-CSDN博客)简单一点,我觉得将这道爬楼梯的题目作为动态规划初期入门的题目还是不错的,比较考验寻找状态转移方程的以及根据题意初始化dp数组的能力。可能是不久前就总结过相关方面的东西,所以这道题没花多少时间就做出来了,而且一次就提交通过了。

首先当然是从题意中判断出该题可以用动态规划的思路来做,这算是比较关键的一步吧,不然后面就玩不下去了。总体思路还是根据之前总结的动态规划的套路,先创建一个在动态规划题目中经常见到的dp数组并明确其中保存的dp[i]指的是什么。该题中,dp[i]是用于保存爬到第i级台阶有几种方案。比如根据示例就可以知道dp[2]=2dp[3]=3

然后就是推导状态转移方程。这里用到了分治的思想,也就是所谓的大事化小,也有点递归的那种味道。具体到这道题目中就可以做如下思考:假设我在第k级,那么让时空倒流一下,想想我在爬到第k级之前的可能站在第几级呢?答案有两种:要么我站在第k-1级台阶,然后往上一次性爬了一级就到了第k级台阶;要么我站在第k-2级台阶,然后往上一次性爬了两级就到了第k级台阶。假设你爬到第k-1级可以有x种方案,而你爬到k-2级可以有y种方案,那么你爬到第k级就可以有x+y种方案了。由此也就可以推导出状态转移方程:dp[i] = dp[i-1] + dp[i-2]

注意:我一开始是把状态转移方程写成了dp[i] = Math.max(dp[i-1] + 1, dp[i-2] + 1),结果可想而知当然是错的,而且错得非常明显,主要还是受到了我之前做的那个题目的影响而没有完全地根据这道题的题意来推导状态转移方程,也算是长了个教训:不要完全被经验主义牵着鼻子走,要实事求是地分析问题。

然后就是确定初始状态了。一般来说确定初始状态和确定之后的遍历顺序是放在一起思考的。这道题我们很容易就可以发现遍历顺序是由前到后(可以比较简单地理解成包裹着状态转移方程的那个for循环中的i是从小变到大(由前向后遍历),还是从大变到小(由后向前遍历))。然后就可以根据题意推导出初始值为dp[1]=1,dp[2]=2

这里也有一个值得推敲的地方,那就是初始化时为什么不是dp[0]=1,dp[1]=1?把这个初始化条件代入状态转移方程,最后也能够获得正确的结果,那为什么不这样做呢?其实我一开始就是写成了这样,因为事实上我是先写的那个for循环(也就是上面所谓的确定遍历顺序那一步),然后再开始考虑初始化条件。其实一开始,我将for循环中的i值初始化为1,但很快我发现了问题:要是i值初始化为1,状态转移方程dp[i] = dp[i-1] + dp[i-2]中的dp[i-2]就会有下标越界的现象,所以我把i的初始值改为2,这时我还没有到考虑dp[0]的那一步。

但当我将i的初始值改为2后,我开始逐渐发现了事情不大对劲。因为当我把i的初始值改为2后,我心里是理所当然地认为dp[0]是有意义的,或者说我自然而然地认为dp[0]理应有一个值。基于这种想法,我就开始想方设法地给dp[0]找一个值(注意,这里我并不是从实际题意出发来确定dp[0]的值,而是我根据一种自以为是的期盼来强行给dp[0]找一个我想要的值)。一开始我给dp[0]赋值为0,因为我想:既然是第0阶,按自然就是0种方法嘛。但很快,我发现这个给dp[0]赋值为0的话,状态转移方程无法计算出后续正确的dp[i]的值(比如:当dp[0]=0,dp[1]=1作为初始条件时,可由状态转移方程计算出dp[2]1,这显然是不对的嘛)。于是我又dp[0]的值改为了1,这次代入方程总算是可以有正确的结果了,在LeetCode里运行也是可以出正确结果的,我心里也松了口气。虽然我觉得dp[0]=1非常奇怪和别扭,感觉很不符合实际(事后,我回想到这里总感觉有点讽刺,我这时候还是自以为是地认为我是在实事求是、从实际题意出发来解决问题的),但是起码算是有结果了。但是问题的关键才刚刚显露……

我越想越觉得不舒服,为啥非要给dp[0]赋值为1才能通过呢?出于疑惑,我开始再次理解题意并推敲dp[i]的含义。终于,还是得出结论:dp[0]=1是没有什么实际意义的,而且其实压根就可以不考虑dp[0]嘛,我直接dp数组的有效值是从dp[1]开始的不就可以了吗?介于此,我把dp数组的初始化条件由原来的dp[0]=1,dp[1]=1改为了dp[1]=1,dp[2]=2我又顺藤摸瓜发现了要将for循环中的i的初始值由2变为3。正当我洋洋得意地回味自己这自以为绝妙的发现的时候,幕后boss终于露面了……

做完上面的修改后,我以为这下总算是搞定了把,开始观赏自己的胜利品——又看了一遍题目,好家伙,这不看不要紧,一看吓一跳,这原题目中明晃晃地就写着几个大字:吃人(bushi)给定 n 是一个正整数,好家伙,我直呼一个好家伙,原来题目早就跟你说了n是一个正整数,所以自然而然地不去考虑dp[0]的情况了嘛……岂可修,这一波操作猛如虎,最后结果居然是小丑竟是我自己……

总的来说,在前面对动态规划做了总结的基础上,这道题并不是很难想到解决思路,所以我一开始说这道题没花多少时间就提交通过了,这其中也包括了我上面发现问题并解决的时间,所以说这道题在动态规划方面给我的带来的收获倒是其次(dp数组的初始化问题以及遍历顺序问题还是受益匪浅的~),最重要的是解题过程中,我深刻地体会到看待问题一定要实事求是的道理,也就是上面我说的那个教训:不要完全被经验主义牵着鼻子走,要实事求是地分析问题。这个过程中我三番两次体会到这句话的正确性,也算是给我后续的人生提了个醒吧……(可恶,竟有某种人生哲学的味道……)

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
  // 创建dp数组并明确其含义
  let dp = [];
  // 初始化dp数组,具体思考过程参考上文
  dp[1] = 1;
  dp[2] = 2;
  // 开始遍历,具体思考过程参考上文,注意i的结束条件应该为 i<=n
  for(let i = 3; i <= n; i++) {
  	// 确定状态转移方程
    dp[i] = dp[i-1] + dp[i-2];
  }
  // 根据dp的定义,dp[n]即为我们想要的结果,最后将其返回
  return dp[n];
};


提交记录
45 / 45 个通过测试用例
状态:通过
执行用时: 64 ms,在所有 JavaScript 提交中击败了96.10%的用户
内存消耗: 37.5 MB,在所有 JavaScript 提交中击败了90.51%的用户
时间:202172820:00:02

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年7月29日18:49:18

参考:爬楼梯 - 爬楼梯 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年7月29日12:02:30
参考:【算法-LeetCode】53. 最大子序和_赖念安的博客-CSDN博客
更新:2021年8月12日15:50:43
参考:【算法-剑指 Offer】10- I. 斐波那契数列_赖念安的博客-CSDN博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值