LeetCode经典算法面试题 #70:爬楼梯(朴素递归、记忆化递归、动态规划等六种实现方案详解)

目录

  • [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 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 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] = 1dp[2] = 2(或者 dp[0] = 1dp[1] = 1,视下标定义而定)。

2.3 破题关键

  • 明确状态转移方程。
  • 优化空间:由于只依赖前两个状态,可以用滚动变量代替数组。
  • 进一步优化:利用矩阵快速幂或通项公式可将时间复杂度降至 O(log n)。

3. 算法设计与实现

3.1 解法一:朴素递归(自顶向下)

核心思想

直接根据递推公式递归求解,将大问题分解为子问题。

算法思路

  1. 定义递归函数 climb(n) 返回到达第 n 阶的方法数。
  2. 终止条件:n == 1 返回 1n == 2 返回 2
  3. 否则返回 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 解法二:记忆化递归(自顶向下 + 缓存)

核心思想

使用数组缓存已计算过的结果,避免重复递归。

算法思路

  1. 创建一个长度为 n+1 的数组 memo,初始值为 0
  2. 递归函数中,若 memo[n] != 0 则直接返回。
  3. 否则计算并存储结果后返回。

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 解法三:动态规划(自底向上)

核心思想

使用数组迭代计算,从基础状态递推到目标状态。

算法思路

  1. 创建 dp 数组,长度为 n+1
  2. 初始化 dp[1] = 1dp[2] = 2
  3. i = 3n,执行 dp[i] = dp[i-1] + dp[i-2]
  4. 返回 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 解法四:空间优化动态规划(滚动数组)

核心思想

由于递推只依赖前两个状态,可以用两个变量代替数组。

算法思路

  1. 初始化 first = 1(对应 dp[1]),second = 2(对应 dp[2])。
  2. i = 3n,计算 third = first + second,然后更新 first = secondsecond = third
  3. 最后返回 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) 时间内求解。

算法思路

  1. 递推关系 [f(n), f(n-1)]^T = [[1, 1], [1, 0]] * [f(n-1), f(n-2)]^T
  2. 因此 [f(n), f(n-1)]^T = M^(n-2) * [f(2), f(1)]^T,其中 M = [[1, 1], [1, 0]]
  3. 使用快速幂计算矩阵的幂。

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 项。

算法思路

  1. 斐波那契数列 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))。
  2. 使用公式:F(n) = (phi^n - psi^n) / sqrt(5),其中 phi = (1+sqrt(5))/2psi = (1-sqrt(5))/2
  3. 由于浮点数精度问题,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 个台阶

题目描述

每次可以爬 123 个台阶,求到达第 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 个台阶,但相邻两步不能相同

题目描述

每次可以爬 12 个台阶,但不能连续两次走相同步数(即不能连续两个 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 阶开始,每次爬 12 阶,到达楼顶(超过 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 变体四:带障碍物的爬楼梯

题目描述

有一个楼梯,某些台阶损坏(不可踩),每次爬 12 阶,求到达楼顶的方法数。

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) 依然成立。

相关推荐
我材不敲代码2 小时前
OpenCV 光流估计实战:Lucas-Kanade 算法实现运动目标跟踪
opencv·算法·目标跟踪
计算机安禾2 小时前
【数据结构与算法】第12篇:栈(二):链式栈与括号匹配问题
c语言·数据结构·c++·学习·算法·visual studio code·visual studio
散峰而望3 小时前
【数据结构】单调栈与单调队列深度解析:从模板到实战,一网打尽
开发语言·数据结构·c++·后端·算法·github·推荐算法
qwehjk20083 小时前
内存泄漏自动检测系统
开发语言·c++·算法
tankeven3 小时前
HJ153 实现字通配符*
c++·算法
旖-旎3 小时前
位运算(两整数之和)(3)
c++·算法·leetcode·位运算
2301_816651223 小时前
C++与Rust交互编程
开发语言·c++·算法
ab1515173 小时前
3.28完成9、16、20、98、100、55、57
算法
带娃的IT创业者3 小时前
营养食谱推荐引擎:基于规则与协同过滤的混合算法
算法·规则引擎·协同过滤·健康管理·食谱推荐·营养搭配·家庭饮食