【LeetCode Hot100 刷题日记 (81/100)】70. 爬楼梯 —— 动态规划 、数学、斐波那契数列🧠

10 阅读5分钟

📌 题目链接:70. 爬楼梯 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:动态规划、数学、斐波那契数列

⏱️ 目标时间复杂度:O(n)(基础解法),可优化至 O(log n)

💾 空间复杂度:O(1)


🧩 题目分析

题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶?

这是一个经典的组合计数问题,表面是“爬楼梯”,实则隐藏着斐波那契数列的结构。

观察示例:

  • n = 2[1+1, 2] → 2 种
  • n = 3[1+1+1, 1+2, 2+1] → 3 种

你会发现:第 n 阶的方法数 = 第 n-1 阶 + 第 n-2 阶
因为最后一步要么走 1 步(来自 n-1),要么走 2 步(来自 n-2)。

这正是斐波那契递推关系

💡 面试考点

  • 能否识别出动态规划模型?
  • 是否知道这是斐波那契数列的变体?
  • 能否从 O(n) 优化到 O(log n)?
  • 是否了解通项公式与精度问题?

🧠 核心算法及代码讲解

✅ 方法一:动态规划(滚动数组优化)

我们定义 f(n) 表示爬到第 n 阶的方法数。

状态转移方程

f(n) = f(n-1) + f(n-2)

边界条件

  • f(0) = 1(站在地面,算一种方式)
  • f(1) = 1(只能走 1 步)

由于 f(n) 只依赖前两项,我们无需开数组,只需三个变量滚动更新:

// 核心算法:动态规划 + 滚动数组
int climbStairs(int n) {
    int p = 0, q = 0, r = 1;   // p = f(i-2), q = f(i-1), r = f(i)
    for (int i = 1; i <= n; ++i) {
        p = q;                 // 更新 f(i-2)
        q = r;                 // 更新 f(i-1)
        r = p + q;             // f(i) = f(i-1) + f(i-2)
    }
    return r;                  // 返回 f(n)
}

🔍 为什么初始化 r = 1
因为 i=1 时,r = f(1) = 1。而 pq 初始为 0,对应 f(-1)=0, f(0)=1 的逻辑(实际我们让 q 在第一次循环后变成 1)。


⚡ 方法二:矩阵快速幂(进阶)

n 极大(如 1e18)时,O(n) 不够快。我们可以用矩阵快速幂将时间复杂度降至 O(log n)

由递推式: $$ \begin{bmatrix} f(n+1) \ f(n) \end{bmatrix}

\begin{bmatrix}
1 & 1 \
1 & 0
\end{bmatrix}
\cdot
\begin{bmatrix}
f(n) \
f(n-1)
\end{bmatrix}

# 可得: $$ \begin{bmatrix} f(n+1) \ f(n) \end{bmatrix} # \begin{bmatrix} 1 & 1 \ 1 & 0 \end{bmatrix}^n \cdot \begin{bmatrix} f(1) \ f(0) \end{bmatrix} M^n \cdot \begin{bmatrix} 1 \ 1 \end{bmatrix}

因此,f(n) = M^{n}[0][0](因为 f(1)=1, f(0)=1,且 M^n * [1,1]^T 的第一个元素就是 f(n+1),但注意官方解法中直接返回 res[0][0] 对应 f(n),需对齐初始条件)。

✅ 实际上,若定义 f(0)=1, f(1)=1,则 f(n) = M^n[0][0] 成立。


📐 方法三:通项公式(数学解法)

斐波那契数列通项(Binet 公式):

f(n)=15[(1+52)n+1(152)n+1]f(n) = \frac{1}{\sqrt{5}} \left[ \left( \frac{1+\sqrt{5}}{2} \right)^{n+1} - \left( \frac{1-\sqrt{5}}{2} \right)^{n+1} \right]

但由于浮点精度问题,仅适用于较小的 n(如 n ≤ 45) ,否则会因舍入误差出错。


🧭 解题思路(分步)

  1. 理解问题:每次走 1 或 2 步,求总方案数。

  2. 找规律:手动计算 n=1,2,3,4 → 发现 1,2,3,5... 是斐波那契数列。

  3. 建模:设 f(n) 为到第 n 阶的方法数 → f(n) = f(n-1) + f(n-2)

  4. 确定边界f(0)=1, f(1)=1

  5. 选择算法

    • 基础:迭代 + 滚动数组(O(n) 时间,O(1) 空间)
    • 进阶:矩阵快速幂(O(log n))
    • 数学:通项公式(慎用)
  6. 编码实现:优先使用滚动数组,简洁高效。


📊 算法分析

方法时间复杂度空间复杂度适用场景
递归(无记忆)O(2ⁿ)O(n)❌ 不可用
记忆化搜索 / DP 数组O(n)O(n)小 n
滚动数组 DPO(n)O(1)✅ 推荐(n ≤ 45)
矩阵快速幂O(log n)O(1)✅ 超大 n(如 1e18)
通项公式O(1)(假设 pow 为 O(1))O(1)⚠️ 有精度风险

💬 面试建议

  • 先写出 O(n) 滚动数组解法(清晰、无 bug)
  • 再讨论能否优化到 O(log n),展示知识广度
  • 提及通项公式但指出其局限性,体现严谨性

💻 代码

C++

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 方法一:动态规划(滚动数组)
class Solution {
public:
    int climbStairs(int n) {
        int p = 0, q = 0, r = 1;
        for (int i = 1; i <= n; ++i) {
            p = q; 
            q = r; 
            r = p + q;
        }
        return r;
    }
};

// 方法二:矩阵快速幂(可选实现)
class SolutionMatrix {
public:
    vector<vector<ll>> multiply(vector<vector<ll>>& a, vector<vector<ll>>& b) {
        vector<vector<ll>> c(2, vector<ll>(2));
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) {
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
            }
        }
        return c;
    }

    vector<vector<ll>> matrixPow(vector<vector<ll>> a, int n) {
        vector<vector<ll>> ret = {{1, 0}, {0, 1}}; // 单位矩阵
        while (n > 0) {
            if ((n & 1) == 1) {
                ret = multiply(ret, a);
            }
            n >>= 1;
            a = multiply(a, a);
        }
        return ret;
    }

    int climbStairs(int n) {
        vector<vector<ll>> base = {{1, 1}, {1, 0}};
        vector<vector<ll>> res = matrixPow(base, n);
        return (int)res[0][0];
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    // 测试用例
    cout << sol.climbStairs(2) << "\n"; // 输出: 2
    cout << sol.climbStairs(3) << "\n"; // 输出: 3
    cout << sol.climbStairs(5) << "\n"; // 输出: 8

    return 0;
}

JS

// JavaScript 版本(滚动数组)
var climbStairs = function(n) {
    let p = 0, q = 0, r = 1;
    for (let i = 1; i <= n; ++i) {
        p = q;
        q = r;
        r = p + q;
    }
    return r;
};

// 测试
console.log(climbStairs(2)); // 2
console.log(climbStairs(3)); // 3
console.log(climbStairs(5)); // 8

// JavaScript 版本(矩阵快速幂)
const multiply = (a, b) => {
    const c = new Array(2).fill(0).map(() => new Array(2).fill(0));
    for (let i = 0; i < 2; i++) {
        for (let j = 0; j < 2; j++) {
            c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
        }
    }
    return c;
}

const matrixPow = (a, n) => {
    let ret = [[1, 0], [0, 1]];
    while (n > 0) {
        if ((n & 1) === 1) {
            ret = multiply(ret, a);
        }
        n >>= 1;
        a = multiply(a, a);
    }
    return ret;
}

var climbStairsMatrix = function(n) {
    const base = [[1, 1], [1, 0]];
    const res = matrixPow(base, n);
    return res[0][0];
};

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!