1. 问题描述与理解
Leetcode第70题"爬楼梯"是一道经典的动态规划入门题目。题目描述很简单:假设你正在爬楼梯,需要n阶才能到达楼顶。每次你可以爬1或2个台阶。问有多少种不同的方法可以爬到楼顶?
这道题看似简单,却蕴含着深刻的算法思想。我第一次遇到这个问题时,直觉反应是用递归解决,但很快就发现递归解法在n较大时会出现严重的性能问题。这促使我深入思考更优的解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础解法分析
2.1 递归解法
最直观的解法是递归。爬到第n阶的方法数等于爬到第n-1阶的方法数(最后一步爬1阶)加上爬到第n-2阶的方法数(最后一步爬2阶)。这给出了递推公式:
f(n) = f(n-1) + f(n-2)
对应的递归实现如下:
python复制def climbStairs(n):
if n == 1:
return 1
if n == 2:
return 2
return climbStairs(n-1) + climbStairs(n-2)
注意:这个解法虽然正确,但时间复杂度是O(2^n),因为每次调用会产生两个子调用。当n=40时,计算量已经非常庞大。
2.2 递归优化:记忆化搜索
为了优化递归解法,可以引入记忆化技术,存储已经计算过的结果:
python复制def climbStairs(n, memo={}):
if n in memo:
return memo[n]
if n == 1:
return 1
if n == 2:
return 2
memo[n] = climbStairs(n-1, memo) + climbStairs(n-2, memo)
return memo[n]
这样时间复杂度降为O(n),因为每个子问题只计算一次。空间复杂度也是O(n),用于存储记忆化结果。
3. 动态规划解法
3.1 自底向上的动态规划
更高效的解法是使用动态规划,从底向上计算:
python复制def climbStairs(n):
if n == 1:
return 1
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
这个解法的时间复杂度是O(n),空间复杂度也是O(n)。但观察发现,我们只需要前两个状态,所以可以进一步优化空间。
3.2 空间优化的动态规划
python复制def climbStairs(n):
if n == 1:
return 1
first, second = 1, 2
for _ in range(3, n + 1):
first, second = second, first + second
return second
优化后空间复杂度降为O(1),只使用常数空间。这是最优的解法之一。
4. 数学解法:斐波那契数列
4.1 问题与斐波那契的关系
仔细观察会发现,爬楼梯问题实际上就是斐波那契数列的变种。斐波那契数列定义为:
F(1) = 1
F(2) = 1
F(n) = F(n-1) + F(n-2) (n > 2)
而爬楼梯问题的解序列是:1, 2, 3, 5, 8... 这实际上是斐波那契数列向后偏移一位。
4.2 通项公式法
斐波那契数列有通项公式(Binet公式):
F(n) = (φ^n - ψ^n) / √5
其中 φ = (1+√5)/2 ≈ 1.618,ψ = (1-√5)/2 ≈ -0.618
因此爬楼梯问题的解可以表示为:
result = (φ^(n+1) - ψ^(n+1)) / √5
Python实现:
python复制def climbStairs(n):
sqrt5 = 5**0.5
phi = (1 + sqrt5) / 2
psi = (1 - sqrt5) / 2
return int((phi**(n+1) - psi**(n+1)) / sqrt5)
这个方法的时间复杂度是O(1),但需要注意浮点数精度问题,当n很大时可能会有误差。
5. 矩阵快速幂解法
5.1 矩阵表示递推关系
斐波那契数列的递推关系可以用矩阵乘法表示:
[ F(n) ] = [1 1][F(n-1)]
[ F(n-1) ] [1 0][F(n-2)]
因此可以通过矩阵快速幂在O(log n)时间内求解。
5.2 Python实现
python复制def climbStairs(n):
def matrix_pow(mat, power):
result = [[1,0],[0,1]] # 单位矩阵
while power > 0:
if power % 2 == 1:
result = matrix_multiply(result, mat)
mat = matrix_multiply(mat, mat)
power //= 2
return result
def matrix_multiply(a, b):
return [
[a[0][0]*b[0][0] + a[0][1]*b[1][0], a[0][0]*b[0][1] + a[0][1]*b[1][1]],
[a[1][0]*b[0][0] + a[1][1]*b[1][0], a[1][0]*b[0][1] + a[1][1]*b[1][1]]
]
if n == 1:
return 1
mat = [[1,1],[1,0]]
result = matrix_pow(mat, n-1)
return result[0][0] + result[0][1]
这个解法的时间复杂度是O(log n),适合非常大的n值。
6. 不同解法的比较
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 递归 | O(2^n) | O(n) | 不推荐 |
| 记忆化递归 | O(n) | O(n) | 教学理解 |
| 动态规划 | O(n) | O(n) | 通用解法 |
| 优化动态规划 | O(n) | O(1) | 推荐解法 |
| 通项公式 | O(1) | O(1) | 小n值 |
| 矩阵快速幂 | O(log n) | O(1) | 极大n值 |
在实际面试中,优化后的动态规划解法(空间O(1))是最常被期望的答案。它平衡了时间复杂度和代码简洁性。
7. 变种问题探讨
7.1 每次可以爬1、2或3阶
如果每次可以爬1、2或3阶,递推公式变为:
f(n) = f(n-1) + f(n-2) + f(n-3)
初始条件:
f(0) = 1
f(1) = 1
f(2) = 2
解法同样可以使用动态规划,只需稍作修改:
python复制def climbStairs(n):
if n == 0:
return 1
if n == 1:
return 1
if n == 2:
return 2
a, b, c = 1, 1, 2
for _ in range(3, n + 1):
a, b, c = b, c, a + b + c
return c
7.2 有障碍物的爬楼梯
Leetcode第63题"不同路径II"的变种。如果在某些台阶设置了障碍物(不能踏上去),如何计算路径数?
解法是在动态规划过程中,遇到障碍物就将该位置的方法数设为0:
python复制def climbStairsWithObstacles(obstacles):
n = len(obstacles)
if n == 0:
return 0
if obstacles[0] == 1:
return 0
dp = [0] * n
dp[0] = 1
for i in range(1, n):
if obstacles[i] == 1:
dp[i] = 0
else:
ways = dp[i-1]
if i >= 2:
ways += dp[i-2]
dp[i] = ways
return dp[-1]
8. 面试技巧与注意事项
-
从简单解法开始:先提出递归解法,然后分析其缺点,再逐步优化。这展示了你解决问题的思路。
-
边界条件:特别注意n=0,1,2的情况。面试官常考察边界条件处理。
-
变量命名:使用有意义的变量名,如prev, curr比a, b更易理解。
-
空间优化:在给出O(n)空间解法后,主动提出可以优化到O(1)空间。
-
数学知识:如果时间允许,可以提及斐波那契关系和通项公式,展示数学素养。
-
测试用例:给出几个测试用例验证你的代码,如n=3(3种), n=4(5种)。
9. 实际应用场景
爬楼梯问题看似简单,但其解法可以应用于许多实际问题:
-
投资组合选择:决定每次投资1单位还是2单位资金,类似于选择爬1阶或2阶。
-
文本换行算法:在特定位置换行,类似于在特定台阶选择休息。
-
机器人路径规划:机器人每次移动有限制的步数,计算到达目标的路径数。
-
密码破解:某些密码系统尝试步进式破解,类似于逐步爬楼梯。
10. 刷题建议
-
同类题目练习:
- Leetcode 746:最小花费爬楼梯
- Leetcode 91:解码方法
- Leetcode 62:不同路径
-
学习资源推荐:
- 《算法导论》动态规划章节
- 《剑指Offer》递归与动态规划专题
- Leetcode动态规划学习卡片
-
练习策略:
- 先独立尝试解决,不要立即看答案
- 写出至少两种解法
- 分析时间/空间复杂度
- 考虑边界条件和异常输入
这道题教会我们,看似简单的问题可能有多种解法,每种解法都有其适用场景。在实际编程中,我们需要根据具体需求选择最合适的解法。对于面试而言,掌握从暴力解法到最优解法的逐步优化过程,比单纯记住最优解法更重要。
