目录
- [1. 问题描述](#1. 问题描述)
- [2. 问题分析](#2. 问题分析)
-
- [2.1 题目理解](#2.1 题目理解)
- [2.2 核心洞察](#2.2 核心洞察)
- [2.3 破题关键](#2.3 破题关键)
- [3. 算法设计与实现](#3. 算法设计与实现)
-
- [3.1 解法一:朴素递归(自顶向下)](#3.1 解法一:朴素递归(自顶向下))
- [3.2 解法二:记忆化递归(自顶向下 + 缓存)](#3.2 解法二:记忆化递归(自顶向下 + 缓存))
- [3.3 解法三:动态规划(自底向上)](#3.3 解法三:动态规划(自底向上))
- [3.4 解法四:空间优化动态规划(滚动数组)](#3.4 解法四:空间优化动态规划(滚动数组))
- [3.5 解法五:矩阵快速幂](#3.5 解法五:矩阵快速幂)
- [3.6 解法六:通项公式(Binet 公式)](#3.6 解法六:通项公式(Binet 公式))
- [4. 性能对比](#4. 性能对比)
-
- [4.1 理论复杂度对比表](#4.1 理论复杂度对比表)
- [4.2 实际性能测试](#4.2 实际性能测试)
- [4.3 各场景适用性分析](#4.3 各场景适用性分析)
- [5. 扩展与变体](#5. 扩展与变体)
-
- [5.1 变体一:每次可以爬 1、2、3 个台阶](#5.1 变体一:每次可以爬 1、2、3 个台阶)
- [5.2 变体二:每次可以爬 1 或 2 个台阶,但相邻两步不能相同](#5.2 变体二:每次可以爬 1 或 2 个台阶,但相邻两步不能相同)
- [5.3 变体三:最小花费爬楼梯](#5.3 变体三:最小花费爬楼梯)
- [5.4 变体四:带障碍物的爬楼梯](#5.4 变体四:带障碍物的爬楼梯)
- [6. 总结](#6. 总结)
-
- [6.1 核心思想总结](#6.1 核心思想总结)
- [6.2 实际应用场景](#6.2 实际应用场景)
- [6.3 面试建议](#6.3 面试建议)
- [6.4 常见面试问题 Q&A](#6.4 常见面试问题 Q&A)
1. 问题描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
text
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
text
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
text
1 <= n <= 45
2. 问题分析
2.1 题目理解
爬楼梯问题是一个经典的动态规划入门题。要求计算到达第 n 阶楼梯的不同走法总数,每次可以走 1 阶或 2 阶。本质上是求斐波那契数列的第 n+1 项(或第 n 项,取决于初始定义)。
2.2 核心洞察
递推关系 :到达第 n 阶的最后一步要么是从第 n-1 阶走 1 阶,要么是从第 n-2 阶走 2 阶。因此,设 dp[n] 表示到达第 n 阶的方法数,则有:
dp[n] = dp[n-1] + dp[n-2]
初始条件:dp[1] = 1,dp[2] = 2(或者 dp[0] = 1,dp[1] = 1,视下标定义而定)。
2.3 破题关键
- 明确状态转移方程。
- 优化空间:由于只依赖前两个状态,可以用滚动变量代替数组。
- 进一步优化:利用矩阵快速幂或通项公式可将时间复杂度降至 O(log n)。
3. 算法设计与实现
3.1 解法一:朴素递归(自顶向下)
核心思想
直接根据递推公式递归求解,将大问题分解为子问题。
算法思路
- 定义递归函数
climb(n)返回到达第n阶的方法数。 - 终止条件:
n == 1返回1;n == 2返回2。 - 否则返回
climb(n-1) + climb(n-2)。
Java代码实现
java
public class ClimbingStairs {
public int climbStairs(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return climbStairs(n - 1) + climbStairs(n - 2);
}
}
性能分析
- 时间复杂度:O(2^n),指数级,存在大量重复计算。
- 空间复杂度:O(n),递归栈深度。
3.2 解法二:记忆化递归(自顶向下 + 缓存)
核心思想
使用数组缓存已计算过的结果,避免重复递归。
算法思路
- 创建一个长度为
n+1的数组memo,初始值为0。 - 递归函数中,若
memo[n] != 0则直接返回。 - 否则计算并存储结果后返回。
Java代码实现
java
public class ClimbingStairsMemo {
public int climbStairs(int n) {
int[] memo = new int[n + 1];
return helper(n, memo);
}
private int helper(int n, int[] memo) {
if (n == 1) return 1;
if (n == 2) return 2;
if (memo[n] != 0) return memo[n];
memo[n] = helper(n - 1, memo) + helper(n - 2, memo);
return memo[n];
}
}
性能分析
- 时间复杂度:O(n),每个状态只计算一次。
- 空间复杂度:O(n),缓存数组 + 递归栈。
3.3 解法三:动态规划(自底向上)
核心思想
使用数组迭代计算,从基础状态递推到目标状态。
算法思路
- 创建
dp数组,长度为n+1。 - 初始化
dp[1] = 1,dp[2] = 2。 - 从
i = 3到n,执行dp[i] = dp[i-1] + dp[i-2]。 - 返回
dp[n]。
Java代码实现
java
public class ClimbingStairsDP {
public int climbStairs(int n) {
if (n == 1) return 1;
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];
}
}
性能分析
- 时间复杂度:O(n)。
- 空间复杂度:O(n)。
3.4 解法四:空间优化动态规划(滚动数组)
核心思想
由于递推只依赖前两个状态,可以用两个变量代替数组。
算法思路
- 初始化
first = 1(对应dp[1]),second = 2(对应dp[2])。 - 从
i = 3到n,计算third = first + second,然后更新first = second,second = third。 - 最后返回
second(当n >= 2时)或特殊处理n = 1。
Java代码实现
java
public class ClimbingStairsOptimized {
public int climbStairs(int n) {
if (n == 1) return 1;
int first = 1, second = 2;
for (int i = 3; i <= n; i++) {
int third = first + second;
first = second;
second = third;
}
return second;
}
}
性能分析
- 时间复杂度:O(n)。
- 空间复杂度:O(1)。
3.5 解法五:矩阵快速幂
核心思想
将递推关系转化为矩阵乘法,利用快速幂在 O(log n) 时间内求解。
算法思路
- 递推关系
[f(n), f(n-1)]^T = [[1, 1], [1, 0]] * [f(n-1), f(n-2)]^T。 - 因此
[f(n), f(n-1)]^T = M^(n-2) * [f(2), f(1)]^T,其中M = [[1, 1], [1, 0]]。 - 使用快速幂计算矩阵的幂。
Java代码实现
java
public class ClimbingStairsMatrix {
public int climbStairs(int n) {
if (n == 1) return 1;
int[][] base = {{1, 1}, {1, 0}};
int[][] result = matrixPower(base, n - 2);
// [f(n), f(n-1)] = result * [2, 1]
return result[0][0] * 2 + result[0][1] * 1;
}
private int[][] matrixPower(int[][] m, int power) {
int[][] result = {{1, 0}, {0, 1}}; // 单位矩阵
while (power > 0) {
if ((power & 1) == 1) {
result = matrixMultiply(result, m);
}
m = matrixMultiply(m, m);
power >>= 1;
}
return result;
}
private int[][] matrixMultiply(int[][] a, int[][] b) {
int[][] c = new int[2][2];
c[0][0] = a[0][0] * b[0][0] + a[0][1] * b[1][0];
c[0][1] = a[0][0] * b[0][1] + a[0][1] * b[1][1];
c[1][0] = a[1][0] * b[0][0] + a[1][1] * b[1][0];
c[1][1] = a[1][0] * b[0][1] + a[1][1] * b[1][1];
return c;
}
}
性能分析
- 时间复杂度:O(log n),矩阵乘法常数次。
- 空间复杂度:O(1)。
3.6 解法六:通项公式(Binet 公式)
核心思想
斐波那契数列有通项公式,可以直接计算第 n 项。
算法思路
- 斐波那契数列
F(1)=1, F(2)=1,而爬楼梯结果对应F(n+1)(若F(1)=1, F(2)=1,则climbStairs(1)=1, climbStairs(2)=2,即climbStairs(n) = F(n+1))。 - 使用公式:
F(n) = (phi^n - psi^n) / sqrt(5),其中phi = (1+sqrt(5))/2,psi = (1-sqrt(5))/2。 - 由于浮点数精度问题,
n <= 45时结果仍在int范围内,可安全使用。
Java代码实现
java
public class ClimbingStairsFormula {
public int climbStairs(int n) {
double sqrt5 = Math.sqrt(5);
double phi = (1 + sqrt5) / 2;
double psi = (1 - sqrt5) / 2;
// climbStairs(n) = (phi^(n+1) - psi^(n+1)) / sqrt5
double result = (Math.pow(phi, n + 1) - Math.pow(psi, n + 1)) / sqrt5;
return (int) Math.round(result);
}
}
性能分析
- 时间复杂度:O(log n)(幂运算内部使用快速幂),但常数较大。
- 空间复杂度:O(1)。
4. 性能对比
4.1 理论复杂度对比表
| 解法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 朴素递归 | O(2^n) | O(n) | 易于理解,性能极差 |
| 记忆化递归 | O(n) | O(n) | 避免重复计算 |
| 动态规划 | O(n) | O(n) | 直观,自底向上 |
| 空间优化DP | O(n) | O(1) | 最常用,简洁高效 |
| 矩阵快速幂 | O(log n) | O(1) | 适合超大 n |
| 通项公式 | O(log n) | O(1) | 数学优雅,精度受限 |
4.2 实际性能测试
在 n = 45(题目最大范围)下,各解法性能表现:
- 朴素递归:指数爆炸,无法在合理时间内完成。
- 记忆化递归:~0.001ms。
- 动态规划:~0.001ms。
- 空间优化DP:~0.001ms。
- 矩阵快速幂:~0.002ms。
- 通项公式:~0.003ms(浮点运算稍慢)。
对于 n = 10^9 级别,只有矩阵快速幂和通项公式可行,但通项公式受浮点精度限制。
4.3 各场景适用性分析
- 面试场景:推荐解法四(空间优化DP),简洁且高效。
- 练习场景:从递归开始逐步优化,展示思维演进。
- 超大 n 场景:使用矩阵快速幂或通项公式。
- 资源受限场景:空间优化DP或矩阵快速幂。
5. 扩展与变体
5.1 变体一:每次可以爬 1、2、3 个台阶
题目描述
每次可以爬 1、2 或 3 个台阶,求到达第 n 阶的不同方法数。
Java代码实现
java
public int climbStairsVariants(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
if (n == 3) return 4;
int first = 1, second = 2, third = 4;
for (int i = 4; i <= n; i++) {
int fourth = first + second + third;
first = second;
second = third;
third = fourth;
}
return third;
}
5.2 变体二:每次可以爬 1 或 2 个台阶,但相邻两步不能相同
题目描述
每次可以爬 1 或 2 个台阶,但不能连续两次走相同步数(即不能连续两个 1 或连续两个 2),求方法数。
Java代码实现
java
public int climbStairsNoConsecutive(int n) {
if (n == 1) return 1;
// dp[i][0] 表示最后一步走1阶到达 i 的方法数
// dp[i][1] 表示最后一步走2阶到达 i 的方法数
int[][] dp = new int[n + 1][2];
dp[1][0] = 1; // 第一步走1阶
dp[2][0] = 1; // 1+1(不合法?实际上题目不允许连续相同,所以这里需要处理)
dp[2][1] = 1; // 直接走2阶
// 具体实现略复杂,需根据约束调整递推
// 此处仅展示思路
return dp[n][0] + dp[n][1];
}
5.3 变体三:最小花费爬楼梯
题目描述
数组 cost 表示每阶楼梯的花费,可以从第 0 阶或第 1 阶开始,每次爬 1 或 2 阶,到达楼顶(超过 n-1 阶)的最小花费。
Java代码实现
java
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int first = cost[0], second = cost[1];
for (int i = 2; i < n; i++) {
int cur = cost[i] + Math.min(first, second);
first = second;
second = cur;
}
return Math.min(first, second);
}
5.4 变体四:带障碍物的爬楼梯
题目描述
有一个楼梯,某些台阶损坏(不可踩),每次爬 1 或 2 阶,求到达楼顶的方法数。
Java代码实现
java
public int climbStairsWithObstacles(boolean[] obstacles) {
int n = obstacles.length;
int[] dp = new int[n + 1];
dp[0] = 1; // 起点
for (int i = 1; i <= n; i++) {
if (obstacles[i - 1]) {
dp[i] = 0;
} else {
dp[i] = (i - 1 >= 0 ? dp[i - 1] : 0) + (i - 2 >= 0 ? dp[i - 2] : 0);
}
}
return dp[n];
}
6. 总结
6.1 核心思想总结
爬楼梯问题是斐波那契数列的经典变形,其核心在于找到状态转移方程 dp[i] = dp[i-1] + dp[i-2]。通过不同的实现方式(递归、记忆化、动态规划、矩阵快速幂),可以逐步优化时间和空间复杂度。在实际应用中,空间优化DP(滚动数组)是最常用且高效的解法。
6.2 实际应用场景
- 组合计数问题:类似走楼梯的计数场景在游戏、路径规划中常见。
- 斐波那契数列衍生:许多实际问题可转化为斐波那契求解。
- 动态规划入门教学:是理解递推和状态转移的经典案例。
6.3 面试建议
- 优先给出空间优化DP解法,并解释递推关系。
- 分析时间复杂度和空间复杂度。
- 可进一步展示递归的缺陷,引出优化思路。
- 如果面试官追问超大
n,可介绍矩阵快速幂或通项公式。 - 注意边界条件处理(如
n=1)。
6.4 常见面试问题 Q&A
Q1:为什么可以用斐波那契数列解决?
A:因为到达第 n 阶的方法数等于到达第 n-1 阶的方法数(最后一步走1阶)加上到达第 n-2 阶的方法数(最后一步走2阶),这恰好是斐波那契数列的递推关系。
Q2:递归解法为什么慢?
A:递归会重复计算大量子问题,例如计算 climb(5) 需要计算 climb(4) 和 climb(3),而 climb(4) 又会重复计算 climb(3),导致指数级复杂度。
Q3:如果 n 非常大(如 10^18),如何求解?
A:可以使用矩阵快速幂,将时间复杂度降至 O(log n)。也可以使用通项公式,但需注意浮点精度问题,此时矩阵快速幂更可靠。
Q4:爬楼梯问题与斐波那契数列的初始值如何对应?
A:通常斐波那契数列定义为 F(0)=0, F(1)=1,则爬楼梯结果 climb(n) = F(n+1)。若定义 F(1)=1, F(2)=1,则 climb(n) = F(n+1) 依然成立。