想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第71课:爬楼梯
模块:动态规划 | 难度:Easy ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/cl… 前置知识:无 预计学习时间:15分钟
🎯 题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。问有多少种不同的方法可以爬到楼顶?
示例:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1阶 + 1阶 + 1阶
2. 1阶 + 2阶
3. 2阶 + 1阶
约束条件:
- 1 <= n <= 45
- 求的是方案总数,不需要列出所有方案
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 最小输入 | n=1 | 1 | 只有一种方式(1阶) |
| 小规模 | n=2 | 2 | 两种方式(1+1或2) |
| 中等规模 | n=5 | 8 | 验证递推逻辑 |
| 大规模 | n=45 | 1836311903 | 性能边界,防止整数溢出 |
💡 思路引导
生活化比喻
想象你在爬一个楼梯,只能一次跨1阶或2阶。站在第n阶楼梯上回头想:我是怎么到这儿的?
🐌 笨办法:每次都列举所有可能的路径,比如"1+1+2+1+...",然后数一数有多少种组合。这像是用穷举法列出所有可能的爬楼方式,数量太大时根本算不过来。
🚀 聪明办法:你突然意识到——要到达第n阶,我只可能从第(n-1)阶跨1步,或者从第(n-2)阶跨2步。所以到达第n阶的方法数 = 到达第(n-1)阶的方法数 + 到达第(n-2)阶的方法数。这就是动态规划的核心思想:大问题拆解成小问题,小问题的答案相加得到大问题的答案。
关键洞察
到达第n阶的方法数 = 到达第(n-1)阶的方法数 + 到达第(n-2)阶的方法数
这其实就是经典的斐波那契数列!
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入:一个正整数 n,表示楼梯总阶数
- 输出:一个整数,表示爬到楼顶的不同方法总数
- 限制:每次只能爬1或2个台阶
Step 2:先想笨办法(暴力法)
最直接的想法是用回溯枚举所有可能的爬楼方式:每次选择爬1阶或2阶,当总和等于n时计数加1。
- 时间复杂度:O(2^n) — 每一步有2种选择,总共n步,所以是指数级
- 瓶颈在哪:大量重复计算,比如"先爬1阶再爬2阶"和"先爬2阶再爬1阶"最后都到达第3阶,但方法数被重复计算了
Step 3:瓶颈分析 → 优化方向
在暴力递归中,我们会多次计算"到达第k阶有多少种方法",比如计算到达第5阶时,会递归计算第4阶和第3阶;而计算第4阶时又会递归计算第3阶——第3阶被重复计算了!
- 核心问题:大量重复子问题被重复求解
- 优化思路:能不能把已经算过的子问题结果记录下来,遇到相同子问题直接查表?→用动态规划(DP)
Step 4:选择武器
- 选用:动态规划(DP)
- 理由:爬楼梯问题具有最优子结构(大问题依赖小问题)和重叠子问题(子问题被重复计算),这正是DP的适用场景
🔑 模式识别提示:当题目出现"有多少种方法"、"计数问题"且存在递推关系时,优先考虑"动态规划"
🔑 解法一:递归(直觉法)
思路
直接按照递推关系写递归:climbStairs(n) = climbStairs(n-1) + climbStairs(n-2),递归边界是 n=1 时返回1, n=2 时返回2。
图解过程
计算 climbStairs(5) 的递归树:
f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1)
/ \
f(2) f(1)
可以看到 f(3) 被计算了2次, f(2) 被计算了3次,产生大量重复计算!
Python代码
def climbStairs(n: int) -> int:
"""
解法一:递归(超时警告)
思路:直接按照递推关系递归,但会超时
"""
# 递归边界
if n == 1:
return 1
if n == 2:
return 2
# 递归关系:到达第n阶 = 从第(n-1)阶跨1步 + 从第(n-2)阶跨2步
return climbStairs(n - 1) + climbStairs(n - 2)
# ✅ 测试
print(climbStairs(3)) # 期望输出:3
print(climbStairs(5)) # 期望输出:8
# print(climbStairs(40)) # 会超时,不要运行
复杂度分析
- 时间复杂度:O(2^n) — 每个节点分裂成2个子节点,递归树呈指数增长
- 具体地说:如果 n=40,大约需要 2^40 = 1万亿次操作,即使每秒10亿次运算也要17分钟
- 空间复杂度:O(n) — 递归调用栈的最大深度为n
优缺点
- ✅ 代码简洁,直观反映递推关系
- ❌ 时间复杂度指数级,n>30 就会超时,面试中不可接受
⚡ 解法二:动态规划(DP数组)
优化思路
既然递归中有大量重复计算,我们用一个数组 dp 记录每一阶的方法数,从小到大依次计算,每个子问题只算一次。
💡 关键想法:用空间换时间,把递归的"自顶向下"改为DP的"自底向上"
图解过程
用 dp[i] 表示到达第 i 阶的方法数
初始化:
dp[1] = 1 (1种方法:1阶)
dp[2] = 2 (2种方法:1+1 或 2阶)
递推:
dp[3] = dp[2] + dp[1] = 2 + 1 = 3
dp[4] = dp[3] + dp[2] = 3 + 2 = 5
dp[5] = dp[4] + dp[3] = 5 + 3 = 8
可视化:
阶数: 1 2 3 4 5
方法: 1 2 3 5 8 ← 斐波那契数列!
Python代码
def climbStairs_dp(n: int) -> int:
"""
解法二:动态规划(DP数组)
思路:用数组记录每一阶的方法数,自底向上计算
"""
# 边界情况
if n <= 2:
return n
# dp[i] 表示到达第 i 阶的方法数
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
# 从第3阶开始递推
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# ✅ 测试
print(climbStairs_dp(3)) # 期望输出:3
print(climbStairs_dp(5)) # 期望输出:8
print(climbStairs_dp(40)) # 期望输出:165580141
复杂度分析
- 时间复杂度:O(n) — 只需遍历一次从1到n,每个子问题只算一次
- 具体地说:如果 n=45,只需 45 次操作,瞬间完成
- 空间复杂度:O(n) — 需要长度为 n+1 的数组
🏆 解法三:动态规划(滚动数组优化,最优解)
优化思路
观察解法二,计算 dp[i] 时只用到了 dp[i-1] 和 dp[i-2],不需要保存整个数组,只需要两个变量即可。
💡 关键想法:空间优化——用两个变量代替整个DP数组
图解过程
用两个变量 prev1 和 prev2 滚动更新:
初始化:
prev2 = 1 (第1阶)
prev1 = 2 (第2阶)
递推过程(以 n=5 为例):
i=3: curr = prev1 + prev2 = 2 + 1 = 3
更新: prev2=2, prev1=3
i=4: curr = prev1 + prev2 = 3 + 2 = 5
更新: prev2=3, prev1=5
i=5: curr = prev1 + prev2 = 5 + 3 = 8
更新: prev2=5, prev1=8
最终返回 prev1 = 8
Python代码
def climbStairs_optimized(n: int) -> int:
"""
解法三:动态规划(滚动数组优化) — 🏆最优解
思路:只用两个变量记录前两个状态,空间优化到O(1)
"""
# 边界情况
if n <= 2:
return n
# 初始化前两阶的方法数
prev2 = 1 # 第1阶
prev1 = 2 # 第2阶
# 从第3阶开始滚动更新
for i in range(3, n + 1):
curr = prev1 + prev2 # 当前阶数的方法数
prev2 = prev1 # 向前滚动
prev1 = curr
return prev1
# ✅ 测试
print(climbStairs_optimized(3)) # 期望输出:3
print(climbStairs_optimized(5)) # 期望输出:8
print(climbStairs_optimized(45)) # 期望输出:1836311903
复杂度分析
- 时间复杂度:O(n) — 与解法二相同,一次遍历
- 空间复杂度:O(1) — 🏆 空间最优! 只使用两个变量,不需要额外数组
🐍 Pythonic 写法
利用 Python 的元组解包,可以让代码更简洁:
# 方法一:元组解包
def climbStairs_pythonic(n: int) -> int:
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
prev2, prev1 = prev1, prev2 + prev1
return prev1 if n > 2 else n
# 方法二:使用 itertools 的 accumulate (进阶)
from itertools import accumulate
def climbStairs_functional(n: int) -> int:
if n <= 2:
return n
fib = list(accumulate(range(n - 1), lambda x, _: x[0] + x[1], initial=(1, 2)))
return fib[-1][1]
这个写法利用了 Python 的元组解包特性,在一行内完成prev2, prev1 = prev1, prev2 + prev1,右边的表达式先计算完再赋值给左边,避免了中间变量 curr。
⚠️ 面试建议:先写清晰版本展示思路,再提 Pythonic 写法展示语言功底。 面试官更看重你的思考过程,而非代码行数。
📊 解法对比
| 维度 | 解法一:递归 | 解法二:DP数组 | 🏆 解法三:滚动数组(最优) |
|---|---|---|---|
| 时间复杂度 | O(2^n) | O(n) | O(n) ← 时间最优 |
| 空间复杂度 | O(n) | O(n) | O(1) ← 空间最优 |
| 代码难度 | 简单 | 简单 | 简单 |
| 面试推荐 | ⭐ | ⭐⭐ | ⭐⭐⭐ ← 首选 |
| 适用场景 | 理解递推关系 | 标准DP入门 | 面试首选,时空双优 |
为什么解法三是最优解:
- 时间复杂度 O(n) 已经是理论最优(至少要计算一次每个阶数)
- 空间复杂度 O(1) 进一步优化,不需要额外数组
- 代码简洁清晰,面试中容易写对且能展示优化能力
面试建议:
- 先用30秒口述暴力递归思路(O(2^n)),表明你能想到基本解法
- 立即优化到解法二的DP数组(O(n)空间),展示动态规划思维
- 重点讲解🏆最优解(滚动数组):"由于只需要前两个状态,可以用两个变量代替数组,将空间优化到O(1)"
- 强调为什么这是最优:时间已达理论下限 O(n),空间进一步压缩到 O(1)
- 手动测试边界用例(n=1, n=2, n=45),展示对解法的深入理解
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下这道爬楼梯的题目。
你:(审题30秒)好的,这道题要求计算爬n阶楼梯有多少种方法,每次可以爬1或2个台阶。让我先想一下...
我的第一个想法是递归:要到达第n阶,我只能从第(n-1)阶跨1步,或从第(n-2)阶跨2步,所以 f(n) = f(n-1) + f(n-2)。但这样会有大量重复计算,时间复杂度是 O(2^n),会超时。
我可以用动态规划优化到 O(n):用一个数组 dp 记录每一阶的方法数,从第1阶开始往上计算。不过其实不需要整个数组,只需要两个变量记录前两个状态就够了,这样空间可以优化到 O(1)。
面试官:很好,请写一下代码。
你:(边写边说关键步骤)
def climbStairs(n):
if n <= 2:
return n
prev2, prev1 = 1, 2 # 初始化第1、2阶的方法数
for i in range(3, n + 1):
prev2, prev1 = prev1, prev2 + prev1 # 滚动更新
return prev1
这里用了Python的元组解包,一行完成状态更新。
面试官:测试一下?
你:用示例 n=3 走一遍:初始 prev2=1, prev1=2,第一次循环计算 prev2+prev1=3,更新后 prev2=2, prev1=3,返回3。再测边界情况 n=1 返回1, n=2 返回2...结果正确。
高频追问
| 追问 | 应答策略 |
|---|---|
| "还有更优解吗?" | "时间 O(n) 已经是最优(至少要算一遍每个阶数),空间 O(1) 也已经最优。如果要追求极致性能,可以用矩阵快速幂把时间降到 O(log n),但面试中通常不需要。" |
| "如果每次可以爬1、2或3个台阶呢?" | "状态转移方程变成 f(n) = f(n-1) + f(n-2) + f(n-3),需要三个变量滚动,初始化需要单独处理前3阶。" |
| "能用公式直接算吗?" | "理论上可以,斐波那契数列有通项公式(涉及黄金比例),但涉及浮点运算和精度问题,不如DP稳定可靠。" |
| "如果n非常大(10^9)呢?" | "O(n) 会超时,需要用矩阵快速幂将时间降到 O(log n),或者直接查表(斐波那契前几百项可以预计算)。" |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1:元组解包 — 一行更新多个变量
prev2, prev1 = prev1, prev2 + prev1 # 右边先全部计算,再赋值给左边
# 技巧2:三元表达式 — 简化边界判断
return prev1 if n > 2 else n
# 技巧3:使用 max/min — 优雅处理边界
dp = [0] * max(3, n + 1) # 至少分配3个元素,避免n=1,2时索引越界
💡 底层原理(选读)
为什么爬楼梯是斐波那契数列?
斐波那契数列的定义是 F(n) = F(n-1) + F(n-2),初始值 F(1)=1, F(2)=1(有的定义是 F(0)=0, F(1)=1)。
爬楼梯的递推关系完全相同:到达第n阶的方法数 = 到达第(n-1)阶的方法数 + 到达第(n-2)阶的方法数。
为什么用滚动数组能优化空间?
在DP中,如果状态转移只依赖前k个状态,就可以只保留这k个状态,不需要存整个数组。爬楼梯中每次只用到前2个状态,所以2个变量就够了。这种技巧叫"滚动数组"或"滚动变量",在很多DP题中都能用到。
算法模式卡片 📐
- 模式名称:线性DP(一维DP)
- 适用条件:当前状态只依赖前面有限个状态,且状态转移是线性的
- 识别关键词:"有多少种方法"、"计数问题"、"递推关系"、"子问题重叠"
- 模板代码:
def linear_dp(n):
# 1. 定义状态: dp[i] 表示到达第 i 个位置的方案数/最值
# 2. 初始化: dp[0], dp[1], ...
# 3. 状态转移: dp[i] = f(dp[i-1], dp[i-2], ...)
# 4. 空间优化: 如果只依赖前k个状态,用k个变量滚动
if n <= base_case:
return base_result
prev_states = [init_values] # 初始化前几个状态
for i in range(start, n + 1):
curr = transition_function(prev_states) # 状态转移
update_prev_states(curr) # 滚动更新
return final_state
易错点 ⚠️
-
边界条件遗漏:忘记处理 n=1 和 n=2 的特殊情况,导致数组越界或结果错误
- 正确做法:在循环前先判断
if n <= 2: return n
- 正确做法:在循环前先判断
-
数组索引越界:dp数组大小定义为
[0] * n会导致访问dp[n]时越界- 正确做法:定义为
[0] * (n + 1),让索引和阶数对应
- 正确做法:定义为
-
滚动变量更新顺序错误:写成
prev1 = prev2 + prev1; prev2 = prev1会导致 prev2 的值被错误更新- 正确做法:用元组解包
prev2, prev1 = prev1, prev2 + prev1或先保存中间变量curr = prev2 + prev1
- 正确做法:用元组解包
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
- 场景1:动态路径规划 — 自动驾驶或机器人路径规划中,计算从起点到终点的最优路径数量,类似的DP思想可以用于计算"有多少条满足约束的路径"
- 场景2:游戏关卡设计 — 游戏中计算玩家通关的可能路线数,或者设计"恰好需要n步才能通关"的关卡,都用到类似的递推计数思想
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 746. 使用最小花费爬楼梯 | Easy | DP递推 | 类似爬楼梯,但需要考虑每一步的花费,求最小花费而非方案数 |
| LeetCode 509. 斐波那契数 | Easy | DP/递归 | 直接求斐波那契数列第n项,与爬楼梯完全相同的递推关系 |
| LeetCode 1137. 第N个泰波那契数 | Easy | DP扩展 | 递推关系变成 f(n) = f(n-1) + f(n-2) + f(n-3),需要三个变量滚动 |
| LeetCode 91. 解码方法 | Medium | DP计数 | 类似爬楼梯,但约束条件更复杂(字符串解码规则),需要考虑多种转移情况 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:如果每次可以爬 1、2 或 3 个台阶,爬到第 n 阶有多少种方法?
💡 提示(实在想不出来再点开)
状态转移方程变成 f(n) = f(n-1) + f(n-2) + f(n-3),需要初始化前三个状态。
✅ 参考答案
def climbStairs_three_steps(n: int) -> int:
"""
每次可以爬1、2或3个台阶
"""
if n == 1:
return 1
if n == 2:
return 2
if n == 3:
return 4 # 1+1+1, 1+2, 2+1, 3
# 需要三个变量滚动
prev3, prev2, prev1 = 1, 2, 4
for i in range(4, n + 1):
curr = prev1 + prev2 + prev3
prev3, prev2, prev1 = prev2, prev1, curr
return prev1
# 测试
print(climbStairs_three_steps(4)) # 输出: 7
print(climbStairs_three_steps(5)) # 输出: 13
核心思路:递推关系从两项和变成三项和,需要维护三个变量 prev3, prev2, prev1 分别代表第 (i-3), (i-2), (i-1) 阶的方法数,然后滚动更新。注意初始化时需要单独处理前3阶(n=3时有4种方法:1+1+1, 1+2, 2+1, 3)。
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。