作为一名算法工程师,我经常遇到新手对动态规划感到困惑的情况。而LeetCode第70题"爬楼梯"恰恰是理解动态规划最经典的入门案例。这道题看似简单,却蕴含着动态规划的核心思想——将复杂问题分解为重叠子问题。
题目描述很简单:假设你正在爬楼梯,需要n阶才能到达楼顶。每次你可以爬1或2个台阶。问有多少种不同的方法可以爬到楼顶?当n=2时,有2种方法(1+1或直接2);当n=3时,有3种方法(1+1+1,1+2,2+1)。这个数列看起来是不是很熟悉?
我们先从最直观的递归思路开始。要到达第n阶台阶,最后一步要么爬1阶(之前已经到达n-1阶),要么爬2阶(之前已经到达n-2阶)。因此,总方法数就是这两种情况的和。
java复制public int climbStairs(int n) {
if(n == 0 || n == 1) return 1;
return climbStairs(n-1) + climbStairs(n-2);
}
这个解法虽然正确,但效率极低。时间复杂度是O(2ⁿ),因为每个调用会产生两个子调用。当n=40时,计算量已经超过万亿次,明显不可行。
注意:递归解法在实际面试中可能会被要求分析其缺点,务必指出它的指数级时间复杂度问题。
观察递归树会发现大量重复计算。比如计算climbStairs(5)需要计算climbStairs(4)和climbStairs(3),而climbStairs(4)又需要计算climbStairs(3)和climbStairs(2),这样climbStairs(3)就被计算了多次。
记忆化搜索通过保存已计算结果来避免重复计算:
java复制public int climbStairs(int n) {
int[] memo = new int[n+1];
return dfs(n, memo);
}
private int dfs(int n, int[] memo) {
if(n == 0 || n == 1) return 1;
if(memo[n] > 0) return memo[n];
memo[n] = dfs(n-1, memo) + dfs(n-2, memo);
return memo[n];
}
这个优化将时间复杂度降到了O(n),因为每个子问题只计算一次。空间复杂度也是O(n)用于存储备忘录。
记忆化搜索是自顶向下的解法,而动态规划通常采用自底向上的方式:
java复制public int climbStairs(int n) {
if(n <= 1) return 1;
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
这里我们定义了dp数组,其中dp[i]表示爬到第i阶的方法数。状态转移方程dp[i] = dp[i-1] + dp[i-2]是核心,初始化条件dp[0]=1和dp[1]=1确保了边界正确性。
观察状态转移方程,我们发现dp[i]只依赖于前两个状态dp[i-1]和dp[i-2],因此不需要存储整个数组:
java复制public int climbStairs(int n) {
if(n <= 1) return 1;
int prev1 = 1, prev2 = 1;
for(int i = 2; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
这个优化将空间复杂度从O(n)降到了O(1),是面试中最推荐的写法。
爬楼梯问题实际上是斐波那契数列的变种。斐波那契数列定义为F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)。而爬楼梯问题的解序列是1,1,2,3,5,8...,相当于斐波那契数列向右平移一位。
知道这个数学关系后,我们还可以使用矩阵快速幂或通项公式来求解,将时间复杂度降到O(log n)或O(1)。不过在面试中,动态规划解法通常就足够了。
在实际面试中,面试官可能会提出各种变种问题来考察理解深度:
步长变化:如果每次可以爬1、2或3个台阶,解法如何修改?
空间限制:如果n很大(比如1e18),如何求解?
路径记录:不仅要计算数量,还要输出所有可能的路径组合
动态规划在现实中有广泛应用:
爬楼梯问题虽然简单,但它包含了动态规划的所有核心要素:
当面试官提出这个问题时,建议按以下结构回答:
新手在解决这个问题时常犯以下错误:
在面试中写出代码后,应该主动测试以下用例:
| 解法 | 时间复杂度 | 空间复杂度 | LeetCode运行时间 |
|---|---|---|---|
| 暴力递归 | O(2ⁿ) | O(n) | 超时 |
| 记忆化搜索 | O(n) | O(n) | 0ms |
| 标准DP | O(n) | O(n) | 0ms |
| 空间优化DP | O(n) | O(1) | 0ms |
虽然我们用Java演示,但在实际工作中可能需要其他语言实现:
Python实现:
python复制def climbStairs(n):
a, b = 1, 1
for _ in range(n):
a, b = b, a + b
return a
C++实现:
cpp复制int climbStairs(int n) {
int prev1 = 1, prev2 = 1;
for(int i = 2; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
JavaScript实现:
javascript复制function climbStairs(n) {
let prev1 = 1, prev2 = 1;
for(let i = 2; i <= n; i++) {
[prev1, prev2] = [prev1 + prev2, prev1];
}
return prev1;
}
爬楼梯问题展示了动态规划的通用解决模式:
掌握这个模式后,可以解决大多数一维动态规划问题,如:
在实际开发中,动态规划常用于优化递归问题、解决组合优化问题等。理解爬楼梯问题的本质,就掌握了动态规划的钥匙。