1. 问题背景与核心思路
第一次在力扣上遇到爬楼梯问题时,我盯着那个看似简单的题目描述愣了半天。题目要求计算爬到n阶楼梯有多少种不同的方法,每次可以爬1或2个台阶。这就像小时候玩跳格子游戏,只不过现在需要用代码精确计算出所有可能性。
动态规划(Dynamic Programming)是解决这类问题的利器。它的核心思想是把大问题分解成小问题,通过解决小问题来构建大问题的解。对于爬楼梯来说,到达第n阶的方法数实际上等于到达第n-1阶和n-2阶方法数的和——因为你最后一步要么跨1阶,要么跨2阶。
关键理解:这个问题符合动态规划的两个基本特征 - 最优子结构(问题的最优解包含子问题的最优解)和重叠子问题(计算过程中会反复求解相同的子问题)。
2. 基础解法与Python实现
2.1 递归解法及其缺陷
最直观的解法是递归:
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时在我的笔记本上运行了将近30秒。这是因为存在大量重复计算,比如计算climbStairs(5)时会重复计算climbStairs(3)多次。
2.2 记忆化递归优化
添加一个缓存字典可以显著提升性能:
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) + climbStairs(n-2) return memo[n]这个版本的时间复杂度降到了O(n),因为每个子问题只计算一次。但递归调用栈的深度仍然是O(n),对于特别大的n可能导致栈溢出。
2.3 标准动态规划实现
更规范的动态规划解法是使用迭代:
def climbStairs(n): if n == 1: return 1 dp = [0]*(n+1) dp[1], dp[2] = 1, 2 for i in range(3, n+1): dp[i] = dp[i-1] + dp[i-2] return dp[n]这里dp数组存储了每个台阶对应的爬法数量。空间复杂度是O(n),但观察发现我们只需要前两个状态,可以进一步优化。
2.4 空间优化版本
只保留前两个状态:
def climbStairs(n): if n == 1: return 1 a, b = 1, 2 for _ in range(3, n+1): a, b = b, a+b return b这个版本空间复杂度降到了O(1),是面试时最推荐的写法。我曾在技术面试中因为写出这个优化版本而获得加分。
3. 数学本质与扩展思考
3.1 斐波那契数列的关联
仔细观察会发现,爬楼梯问题实际上是斐波那契数列的变种。斐波那契数列定义为F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2),而爬楼梯问题是dp(1)=1, dp(2)=2, dp(n)=dp(n-1)+dp(n-2)。因此dp(n) = F(n+1)。
这个认知让我想到可以直接使用斐波那契数列的通项公式(比奈公式)来求解:
def climbStairs(n): sqrt5 = 5**0.5 fibn = ((1+sqrt5)/2)**(n+1) - ((1-sqrt5)/2)**(n+1) return int(fibn/sqrt5)不过由于浮点数精度问题,当n较大时结果可能不准确,实际工程中不太推荐。
3.2 步长变化的一般化问题
如果题目改为每次可以爬1、2或3个台阶,解法只需稍作修改:
def climbStairs(n): if n == 1: return 1 if n == 2: return 2 if n == 3: return 4 a, b, c = 1, 2, 4 for _ in range(4, n+1): a, b, c = b, c, a+b+c return c这个变种在有些公司的笔试中出现过,理解原理后很容易举一反三。
4. 实际应用与性能测试
4.1 不同解法的性能对比
我用timeit模块测试了n=35时各版本的运行时间:
- 朴素递归:约3.6秒
- 记忆化递归:约2.5微秒
- 标准DP:约1.8微秒
- 空间优化DP:约1.2微秒
虽然对于这个具体问题差异不大,但在处理更复杂的DP问题时,空间优化可能带来显著性能提升。
4.2 边界条件处理经验
在实际编码中,我总结出几个常见坑点:
- 忘记处理n=0的情况(有些问题定义dp[0]=1)
- 数组初始化长度应为n+1而非n
- 当n很小时直接返回避免数组越界
一个健壮的实现应该这样写:
def climbStairs(n): if n <= 0: return 0 if n == 1: return 1 if n == 2: return 2 a, b = 1, 2 for _ in range(3, n+1): a, b = b, a+b return b4.3 力扣提交注意事项
在力扣上提交时要注意:
- 函数名必须完全匹配(包括大小写)
- 不要添加额外的print语句
- 确保在本地测试边界条件
- 多次提交相同代码会被限制,要谨慎
我曾在一次周赛中因为把函数名写成"climb_stairs"而白白浪费5分钟调试时间。