--- tags: [算法/动态规划, 基础DP, Fibonacci] create time: 2026-05-16 18:30 --- # 81. 爬楼梯(Climbing Stairs) > [!quote] LeetCode 原题 · 简单 > 假设你正在爬楼梯。需要 `n` 阶你才能到达楼顶。 > 每次你可以爬 **1 或 2** 个台阶。问有多少种不同的方法可以爬到楼顶? > > - 输入范围:`1 <= n <= 45` ## 题面 **示例 1:** ``` 输入:n = 2 输出:2 解释:有两种方法 1. 1 阶 + 1 阶 2. 2 阶 ``` **示例 2:** ``` 输入:n = 3 输出:3 解释:有三种方法 1. 1 + 1 + 1 2. 1 + 2 3. 2 + 1 ``` ## 思路 ### 第一步:找递推关系 先从一个简单的问题开始——**如果最后一步只有一阶或两阶两种可能,那么到达第 i 阶的方式数由什么决定?** 想象你已经站在了第 i 阶的楼顶上,回顾你的最后一步: - 如果你是从第 **i-1** 阶迈了 **1 步**上来的 - 或者从第 **i-2** 阶迈了 **2 步**上来的 除此之外没有别的可能了。所以: > `dp[i] = dp[i-1] + dp[i-2]` 这个递推式是不是非常熟悉?对,它就是 **Fibonacci(斐波那契数列)**。 ```mermaid graph LR A["第 i 阶"] --> B["第 i-1 阶\n迈 1 步"] A --> C["第 i-2 阶\n迈 2 步"] B --> D["dp[i-1] 种方式"] C --> E["dp[i-2] 种方式"] A -.-> F["dp[i] = dp[i-1]\n+ dp[i-2]"] style A fill:#e7f3ff,stroke:#3b82f6 style F fill:#fef3c7,stroke:#f59e0b ``` ### 第二步:确定边界条件 递推关系有了,接下来思考——**哪些位置是"起点",它们的值应该是多少?** - `dp[1] = 1`:只有 1 阶台阶,直接走上去,**只有 1 种方式** - `dp[2] = 2`:有 2 阶台阶,可以 `1+1` 或 `2`,**有 2 种方式** 这就是我们的"地基"。所有更大的 i 都从这两块砖往上垒。 ### 第三步:自底向上填表 以 `n = 5` 为例,逐步填表: | i | 0 | 1 | 2 | 3 | 4 | 5 | |---|---|---|---|---|---|---| | dp[i] | — | **1** | **2** | **3** | **5** | **8** | 每一步都只是前两步之和,规律一目了然。 ```mermaid flowchart LR subgraph "填表过程 n = 5" A["dp[1] = 1"] --> B["dp[2] = 2"] B --> C["dp[3] = 1+2 = 3"] C --> D["dp[4] = 2+3 = 5"] D --> E["dp[5] = 3+5 = 8"] end style A fill:#dbeafe,stroke:#2563eb style E fill:#dcfce7,stroke:#16a34a ``` ## 代码提示 在动手写代码之前,想一想这些关键决策点: > [!question] 💡 思考 1 > 需要一个完整的数组来存储所有 `dp[i]` 吗?还是只需要记住最近两个值就够了? 当你计算 `dp[i]` 时,只用到了 `dp[i-1]` 和 `dp[i-2]`,之前的值全部变成了冗余。这意味着我们可以把空间复杂度从 **O(n)** 压缩到 **O(1)**。 > [!question] 💡 思考 2 > 循环应该从 `i = 3` 开始还是 `i = 1` 开始?为什么 `n = 1` 和 `n = 2` 不需要进循环? 当 `n ≤ 2` 时答案已经直接知道了,循环从第 3 阶开始填表即可。在代码中可以统一处理,也可以用边界判断提前返回。 ## 技巧 ### 滚动变量法(空间优化) 不需要数组!用两个变量滚动更新就够: | 变量 | 含义 | 初始值 | |------|------|--------| | `prev2` | dp[i-2] | 1(dp[1])| | `prev1` | dp[i-1] | 2(dp[2])| 每一步计算 `cur = prev1 + prev2`,然后往前滚: ``` prev2 = prev1 prev1 = cur ``` 就像你在爬楼梯,每次只看眼前这 2 步就够了——**「贪心不看全图,动态只看相邻」**。 ### 递归 vs 迭代 | 方法 | 时间 | 空间 | 评价 | |------|------|------|------| | 朴素递归 | O(2ⁿ) | O(n) | 有大量重复子问题,会超时 | | 记忆化递归 | O(n) | O(n) | 加缓存就行,但递归栈开销不小 | | 自底向上 DP | O(n) | O(n) | 标准写法,清晰直观 | | **滚动变量 DP** | **O(n)** | **O(1)** | **最优推荐 ✅** | ## 代码 ### Go 语言实现 ```go // climbStairs returns the number of distinct ways to climb n stairs. // You can take either 1 or 2 steps at a time. func climbStairs(n int) int { // 边界情况:1 阶或 2 阶直接返回 if n <= 2 { return n } // 滚动变量:只需要记住前两个值 prev2 := 1 // dp[1] prev1 := 2 // dp[2] // 从第 3 阶开始依次计算 for i := 3; i <= n; i++ { cur := prev1 + prev2 // dp[i] = dp[i-1] + dp[i-2] prev2 = prev1 // 滚动向前 prev1 = cur } return prev1 } ``` ### 执行过程演示(n = 5) ``` 初始: prev2=1, prev1=2 i=3: cur=2+1=3 → prev2=2, prev1=3 i=4: cur=3+2=5 → prev2=3, prev1=5 i=5: cur=5+3=8 → prev2=5, prev1=8 返回 prev1 = 8 ✅ ``` ### 复杂度分析 - **时间复杂度:O(n)** — 单层循环,做 n-2 次加法 - **空间复杂度:O(1)** — 仅使用三个整型变量 ## 举一反三 这道题本质上是 **Fibonacci 数列的第一道入门 DP**。它的核心模式 —— **「当前状态 = 前几个状态的组合」** —— 会在很多题目中重复出现: - [[13-最大子数组和]] — 同样是线性 DP,思考"选或不选" - 接龙类问题 — 同样考虑最后一步的多种选择 > [!summary] 📌 本节要点 > 1. 找清递推关系:最后一步只有两种选择 > 2. 确定边界:dp[1]=1, dp[2]=2 > 3. 空间优化:滚动变量替代数组,O(n)→O(1)