1. 爬楼梯问题概述
爬楼梯问题是一个经典的算法问题,题目描述很简单:假设你正在爬楼梯,需要n阶才能到达楼顶。每次你可以爬1或2个台阶。问有多少种不同的方法可以爬到楼顶?
这个问题看似简单,却蕴含着深刻的算法思想。我第一次遇到这个问题时,直觉上觉得可以用递归解决,但很快就发现单纯的递归效率太低。后来通过学习,发现这是一个典型的动态规划问题,也是理解斐波那契数列的绝佳案例。
2. 问题分析与数学建模
2.1 问题本质解析
让我们仔细分析这个问题。要到达第n阶楼梯,只有两种可能的方式:
- 从第n-1阶爬1步上来
- 从第n-2阶爬2步上来
因此,到达第n阶的方法总数就是到达第n-1阶的方法数加上到达第n-2阶的方法数。这正好符合斐波那契数列的定义:
f(n) = f(n-1) + f(n-2)
2.2 边界条件确定
任何递推关系都需要明确的边界条件。对于爬楼梯问题:
- f(1) = 1 (只有一种方法:爬1阶)
- f(2) = 2 (两种方法:1+1或直接爬2阶)
2.3 递归树分析
如果用纯递归解决,会生成一棵递归树。以n=5为例:
code复制 f(5)
/ \
f(4) f(3)
/ \ / \
f(3) f(2) f(2) f(1)
/ \
f(2)f(1)
可以看到f(3)被计算了两次,f(2)被计算了三次。随着n增大,重复计算会呈指数级增长,这就是纯递归效率低下的原因。
3. 解法一:基础动态规划
3.1 算法原理
动态规划通过存储子问题的解来避免重复计算。对于爬楼梯问题:
- 创建一个数组dp,其中dp[i]表示爬到第i阶的方法数
- 初始化dp[1]=1,dp[2]=2
- 对于i从3到n,计算dp[i] = dp[i-1] + dp[i-2]
- 最终返回dp[n]
3.2 Java实现
java复制class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
3.3 复杂度分析
- 时间复杂度:O(n),需要遍历从3到n的所有整数
- 空间复杂度:O(n),需要长度为n+1的数组存储中间结果
注意:在实际面试中,即使你知道更优解法,也应该先给出这种基础解法,然后再优化。这展示了你的思考过程。
4. 解法二:空间优化的动态规划
4.1 优化思路
观察基础动态规划解法,我们发现计算dp[i]只需要dp[i-1]和dp[i-2],不需要保存整个数组。因此可以用三个变量来滚动更新:
- prev2:相当于dp[i-2]
- prev1:相当于dp[i-1]
- curr:当前计算的dp[i]
4.2 Java实现
java复制class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int prev2 = 1; // f(n-2)
int prev1 = 2; // f(n-1)
int curr = 0;
for (int i = 3; i <= n; i++) {
curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return curr;
}
}
4.3 复杂度分析
- 时间复杂度:O(n),同样需要遍历从3到n
- 空间复杂度:O(1),只使用了常数空间
提示:这是面试中最推荐的解法,因为它既高效又节省空间。务必熟练掌握这种滚动数组的技巧。
5. 解法三:记忆化递归
5.1 算法原理
递归是最直观的解法,但会有大量重复计算。记忆化技术通过存储已计算的结果来避免重复计算:
- 创建一个memo数组存储已计算的结果
- 递归计算时,先检查memo中是否已有结果
- 如果没有,则递归计算并存储结果
5.2 Java实现
java复制class Solution {
private int[] memo;
public int climbStairs(int n) {
memo = new int[n + 1];
return climb(n);
}
private int climb(int n) {
if (n <= 2) {
return n;
}
if (memo[n] != 0) {
return memo[n];
}
memo[n] = climb(n - 1) + climb(n - 2);
return memo[n];
}
}
5.3 复杂度分析
- 时间复杂度:O(n),每个子问题只计算一次
- 空间复杂度:O(n),用于memo数组和递归调用栈
注意:虽然记忆化递归的时间复杂度也是O(n),但递归调用会有额外的函数调用开销,在实际应用中通常不如迭代版本的动态规划高效。
6. 进阶思考与变种问题
6.1 数学解法:矩阵快速幂
对于特别大的n(比如n=10^9),我们可以使用矩阵快速幂将时间复杂度降到O(log n)。这种方法基于斐波那契数列的矩阵表示:
code复制[f(n) ] [1 1][f(n-1)]
[f(n-1)] = [1 0][f(n-2)]
通过矩阵的快速幂运算,可以在对数时间内计算出结果。
6.2 变种问题
-
每次可以爬1、2或3个台阶:
状态转移方程变为f(n) = f(n-1) + f(n-2) + f(n-3)
边界条件需要f(1)=1, f(2)=2, f(3)=4 -
某些台阶不能踩(障碍物):
类似跳跃游戏问题,需要标记哪些台阶不能踩,在状态转移时跳过这些台阶 -
最小代价爬楼梯:
每个台阶有一个代价,求爬到顶部的最小总代价
6.3 实际应用场景
爬楼梯问题虽然简单,但其思想广泛应用于:
- 金融领域的期权定价
- 计算机图形学中的路径规划
- 生物信息学中的序列比对
- 网络路由算法
7. 面试技巧与常见错误
7.1 面试常见问题
- 你能解释一下状态转移方程是如何得出的吗?
- 为什么动态规划比纯递归更高效?
- 如何将空间复杂度从O(n)优化到O(1)?
- 如果每次可以爬1、2或3个台阶,如何修改你的解法?
7.2 常见错误
- 边界条件错误:忘记处理n=0或n=1的情况
- 数组越界:创建dp数组时长度应为n+1而不是n
- 整数溢出:当n较大时,结果可能超过int范围,需要使用long
- 递归深度过大:对于大n,纯递归会导致栈溢出
7.3 优化建议
- 先写基础解法,再逐步优化
- 明确说明时间和空间复杂度
- 讨论可能的边界情况和异常处理
- 准备相关的变种问题
我在实际面试中遇到过这个问题的多种变体。有一次面试官要求我不仅计算方法的数量,还要输出所有可能的路径。这需要结合回溯算法来解决,展示了问题可以如何扩展。关键是要理解基础问题的核心思想,这样才能灵活应对各种变体。