题目描述
思路解析
一句话答案:LeetCode 70 爬楼梯的标准解是动态规划递推 dp[i] = dp[i-1] + dp[i-2]:到第 i 阶的最后一步只能跨 1 或 2 阶,两类互斥穷尽,方案数相加。两个变量滚动,时间 O(n)、空间 O(1),本质是斐波那契。
爬楼梯到底在数什么
楼梯共 n 阶,每步只能上 1 阶或 2 阶,问从地面走到第 n 阶一共有多少种不同的走法。注意这是一道「数方案数」的题:答案是走法的总数,而不是最少步数,也不是某条具体路线。比如 n = 10 时答案是 89,意味着存在 89 条互不相同的上楼顺序。
为什么不能把所有走法枚举一遍
最直觉的思路是把每一步的选择都展开:每一步有跨 1 阶、跨 2 阶两个分支,一路分叉下去,走法总数随 n 指数级膨胀,逐条枚举根本数不完。慢的根源是重复劳动——无数条路线的后半段其实一模一样,却被一遍遍重数。
关键观察藏在「最后一步」上:不管前面怎么走,落到第 i 阶的最后一步只有两种可能——从第 i-1 阶跨 1 步上来,或从第 i-2 阶跨 2 步上来。于是「到第 i 阶的方法数」就落在「到 i-1 阶」和「到 i-2 阶」这两个更小的问题上,递推能写了。
dp 数组怎么定义,dp[0] 为什么是 1
定义 dp[i] 为「从地面走到第 i 阶的方法数」。起点要铺两个:dp[1] = 1,因为到第 1 阶只有跨一步这一种走法;dp[0] = 1,表示站在地面、一步没走也算一种方案。dp[0] 常被误设成 0,一旦设 0,dp[2] = dp[1] + dp[0] 就会漏掉「直接跨 2 阶」这条路,错误还会一路传染,整张表全部偏小。
为什么两条来路直接相加不重不漏
加法成立的依据是分类计数:按「最后一步跨几阶」把到达第 i 阶的所有走法分成两堆——最后跨 1 阶的,前半程恰好是一条到 i-1 阶的走法;最后跨 2 阶的,前半程恰好是一条到 i-2 阶的走法。两堆的最后一步不同,不可能有同一条走法被数两次;而最后一步只有这两种可能,也不会有走法被漏掉。这题是数方案不是求最优,转移用加法而不是 min 或 max——同一张递推表换成取最小值,就变成另一类问题了。
拿 n=10 把递推填一遍
亲手填一遍表就彻底通了。地面 dp[0] = 1,第 1 阶 dp[1] = 1。到第 2 阶:最后一步要么从第 1 阶跨 1、要么从地面跨 2,dp[2] = dp[1] + dp[0] = 2。往后每格都是前两格相加:dp[3] = 3、dp[4] = 5、dp[5] = 8,整串就是 1、1、2、3、5、8、13、21、34、55、89,到 dp[10] = 89,正是答案。滚动优化时只留最近两个数,用 a, b = b, a + b(把 a 换成旧的 b、b 换成两者之和)每轮向前挪一格,整张表根本不必存下来。
复杂度多少,和斐波那契什么关系
从 dp[2] 递推到 dp[n],每格只做一次加法,时间 O(n);又因为 dp[i] 只依赖前两项,不必存整个数组,用两个变量轮替即可,空间 O(1)。
递推式 dp[i] = dp[i-1] + dp[i-2] 加上 dp[0] = dp[1] = 1,正是斐波那契数列,所以爬楼梯常被叫作斐波那契的「面试皮肤」。边界上留意 n = 1 要能直接返回 1,循环从第 2 阶才开始。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
状态记录到达每阶的方法数。按最后一步走一阶或两阶分类,两类互不重叠且覆盖全部走法,所以数量相加。
表格列号从零到十,表示目标阶数。先填写两个基础状态,再按阶数递增计算。
到第零阶时没有移动,用一种空序列表示。这使直接跨两阶的走法能被正确计入。
到第一阶只有走一阶这一种方法。两个基础状态现在都已填写。
计算第 2 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 1 种,前两阶有 1 种,相加得到 2 种,写入当前状态。
计算第 3 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 2 种,前两阶有 1 种,相加得到 3 种,写入当前状态。
计算第 4 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 3 种,前两阶有 2 种,相加得到 5 种,写入当前状态。
计算第 5 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 5 种,前两阶有 3 种,相加得到 8 种,写入当前状态。
计算第 6 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 8 种,前两阶有 5 种,相加得到 13 种,写入当前状态。
计算第 7 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 13 种,前两阶有 8 种,相加得到 21 种,写入当前状态。
计算第 8 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 21 种,前两阶有 13 种,相加得到 34 种,写入当前状态。
计算第 9 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 34 种,前两阶有 21 种,相加得到 55 种,写入当前状态。
计算第 10 阶:最后走一阶来自前一阶,最后走两阶来自前两阶,两类数量相加。
前一阶有 55 种,前两阶有 34 种,相加得到 89 种,写入当前状态。
第十阶的方法数为八十九。这个序列满足相邻两项相加的斐波那契递推,但起始下标要按本题状态定义。
先计算第二阶,两个变量分别保存第零阶和第一阶的方法数,都是一。
读取旧值后,右侧得到旧的后一项一,以及两项之和二;左侧变量此时还没改变。
把右侧的两个结果同时保存,前一个变量为一,后一个变量为二,完成第二阶。
下一轮计算第三阶,仍先读取旧的一和二,右侧得到二与三。
正确结果为二与三。若先覆盖第一个变量,再用它求和,就会错误得到四。
一阶、两阶、三阶的答案分别为一、二、三,可以直接列出走法核对。
两类走法按最后一步分类,既不重叠也不遗漏;数组表和滚动变量保存的是同样的状态。
参考代码
class Solution: def climbStairs(self, n): a, b = 1, 1 # dp[i-2], dp[i-1] for _ in range(2, n + 1): a, b = b, a + b return b复杂度
- 时间:O(n),一遍线性递推,每格 O(1)
- 空间:O(1),只需前两项滚动
易错点
面试追问把动画讲成自己的话
追问为什么两条来路能直接相加而不会重复计数?
追问这张表为什么是斐波那契?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
使用最小花费爬楼梯
LeetCode 746 · 简单 · 沿着 一维动态规划 继续往下推进