🏗️ 爬楼梯
📝 题目描述
假设你正在爬楼梯,需要n
阶你才能到达楼顶。
每次你可以爬1
或2
个台阶,你有多少种不同的方法可以爬到楼顶呢?
📋 代码模版
class Solution {
public int climbStairs(int n) {
}
}
function climbStairs(n: number): number {
}
💡 提示
🚀 示例
🖊️ 题解
记忆化搜索
我们可以先考虑最简单的情况。如果楼梯台阶只有一级,那么爬到楼顶的方法总数为1
种,即仅爬1阶。如果楼梯台阶只有二级,那么爬到楼顶的方法总数为2
种,分别为爬1阶 + 1阶、仅爬2阶。
如果楼梯台阶超过了二级(n >= 3
),那么爬到第n
阶的方法总数计算可以分为以下两种情况。
- 爬到第
n - 1
阶有多少种方法? - 爬到第
n - 2
阶有多少种方法?
而爬到第n
阶的方法总数就等于爬到i - 1
阶的方法总数加上爬到i - 2
阶的方法总数。因为如果想要爬到第n
阶,无非就是在第n - 1
阶再爬1阶,或在第n - 2
阶再爬2阶。
在上述过程中,原问题被拆分为两个子问题。通过仔细观察这两个子问题,我们发现它们与原问题相似,只是规模更小,并且当子问题的答案不明确时,我们仍然可以将其进一步分解为更小的子问题。
因此,该题可以使用深度优先搜索dfs
来解决。我们定义
综上所述,我们很容易写出以下代码。
class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
}
function climbStairs(n: number): number {
if (n <= 2) {
return n;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
😭但经过实践,当输入的n
增大到一定阈值时,以上代码的提交大概率会因为超时而无法通过。
那么,导致以上代码运行效率低下的原因是什么呢?接下来,让我们来分析一下它的时间复杂度。假设n = 6
,可以画出以下递归树。
递归树为一颗二叉树。其中,每个节点都代表一个子问题的计算,节点总数为指数级别。因此,我们可以得出结论:随着n
的增大,由于重复计算不断增加,子问题的个数也随之呈指数级增长。基于此,这个算法的时间复杂度为
⭐以上所述为多路递归的典型问题,即重复计算。为了应对这个问题,经典的解决方法是创建一个「备忘录」,在计算每个子问题之前先查找备忘录。如果备忘录中没有记录计算结果,就进行计算并将结果记录在备忘录中;反之,则直接获取记录的值并返回。
tip: 多路递归是指递归函数体中多次调用了自身。
这种带备忘录的递归算法通常也被称为记忆化搜索,对应本题的算法代码如下。
class Solution {
public int climbStairs(int n) {
int[] memory = new int[n + 1];
return dfs(n, memory);
}
private int dfs(int i, int[] memory) {
if (i <= 2) {
return i;
}
if (memory[i] != 0) {
return memory[i];
}
return memory[i] = dfs(i - 1, memory) + dfs(i - 2, memory);
}
}
function climbStairs(n: number): number {
const memory: number[] = [];
const dfs = (i: number): number => {
if (i <= 2) {
return i;
}
if (memory[i]) {
return memory[i];
}
return memory[i] = dfs(i - 1) + dfs(i - 2);
}
return dfs(n);
}
记忆化搜索是一种典型的空间换时间的思想,它是在一颗存在大量冗余的递归树上通过「剪枝」,保证了每个相同的节点只计算一次。因此,该算法的时间复杂度为
动态规划
根据以上dfs
的解法,我们可以很轻松地将其翻译成动态规划dp
解法,具体步骤如下。
- 状态定义:设
为一维数组(逻辑上从索引 1
开始),其中代表⭐️爬到第 i
阶的方法总数⭐️,。 - 初始状态:根据边界条件,初始化
数组的前两项,即 。 - 状态转移方程:
,其中 。 - 返回值:
,即爬到第 n
阶的方法总数。
class Solution {
public int climbStairs(int n) {
if (n <= 1) {
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];
}
}
function climbStairs(n: number): number {
if (n <= 1) {
return n;
}
const dp: number[] = [];
dp[1] = 1;
dp[2] = 2;
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
以上算法需要一个很长的DP table
(
这种技巧通常也被称为状态压缩,对应本题的算法代码如下。
class Solution {
public int climbStairs(int n) {
if (n <= 1) {
return n;
}
int pre = 1, cur = 2;
for (int i = 3; i <= n; i++) {
int sum = pre + cur;
pre = cur;
cur = sum;
}
return cur;
}
}
function climbStairs(n: number): number {
if (n <= 1) {
return n;
}
let pre = 1, cur = 2;
for (let i = 3; i <= n; i++) {
const sum = pre + cur;
pre = cur;
cur = sum;
}
return cur;
}
💭 复杂度分析
基于记忆化搜索
的解决方案的复杂度分析如下。
- 时间复杂度:
。 - 空间复杂度:
。
基于动态规划
的最优(状态压缩)解决方案的复杂度分析如下。
- 时间复杂度:
。 - 空间复杂度:
。