vault backup: 2026-05-16 18:39:36
This commit is contained in:
+193
@@ -0,0 +1,193 @@
|
|||||||
|
---
|
||||||
|
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)
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
tags: [算法/动态规划, 基础DP, 数组模拟, 数学]
|
||||||
|
create time: 2026-05-16 18:45
|
||||||
|
---
|
||||||
|
|
||||||
|
# 82. 杨辉三角(Pascal's Triangle)
|
||||||
|
|
||||||
|
> [!quote] LeetCode 原题 · 简单
|
||||||
|
> 给定一个非负整数 `numRows`,生成「杨辉三角」的前 `numRows` 行。
|
||||||
|
>
|
||||||
|
> 在「杨辉三角」中,每个数是它左上方和右上方的数的和。
|
||||||
|
>
|
||||||
|
> - 输入范围:`1 <= numRows <= 30`
|
||||||
|
|
||||||
|
## 题面
|
||||||
|
|
||||||
|
**示例 1:**
|
||||||
|
|
||||||
|
```
|
||||||
|
输入:numRows = 5
|
||||||
|
输出:[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例 2:**
|
||||||
|
|
||||||
|
```
|
||||||
|
输入:numRows = 1
|
||||||
|
输出:[[1]]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 思路
|
||||||
|
|
||||||
|
### 第一步:观察规律
|
||||||
|
|
||||||
|
拿出一张纸来,随手写几行看看——
|
||||||
|
|
||||||
|
```
|
||||||
|
第 0 行: 1
|
||||||
|
第 1 行: 1 1
|
||||||
|
第 2 行: 1 2 1
|
||||||
|
第 3 行: 1 3 3 1
|
||||||
|
第 4 行: 1 4 6 4 1
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!question] 💡 思考
|
||||||
|
> 仔细观察每一行的**首尾元素**和**中间元素**,你能发现什么共同特征?
|
||||||
|
|
||||||
|
有两个关键观察:
|
||||||
|
|
||||||
|
1. **每行的第一个和最后一个元素都是 1** ——这是边界条件,不需要计算。
|
||||||
|
2. **中间每个元素 = 左上方 + 右上方** ——这就是题目描述的递推关系。
|
||||||
|
|
||||||
|
具体来说,如果用 `triangle[i][j]` 表示第 `i` 行第 `j` 列的元素(从 0 开始),那么:
|
||||||
|
|
||||||
|
> - `triangle[i][0] = triangle[i][i] = 1` (边界)
|
||||||
|
> - `triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j]` (中间元素)
|
||||||
|
|
||||||
|
### 第二步:理解"左上方和右上方"
|
||||||
|
|
||||||
|
用 Mermaid 来看看依赖关系——
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph "构建第 2 行 [1, 2, 1]"
|
||||||
|
A["triangle[2][0]=1\n(固定为1)"] --> C["triangle[2][1]\n= ?"]
|
||||||
|
B["triangle[2][2]=1\n(固定为1)"] -.->|不参与| C
|
||||||
|
C --> D["triangle[2][1]=1+1=2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "值从哪里来?"
|
||||||
|
E["triangle[1][0]=1"] -->|"左上方"| C
|
||||||
|
F["triangle[1][1]=1"] -->|"右上方"| C
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#dbeafe,stroke:#2563eb
|
||||||
|
style B fill:#dbeafe,stroke:#2563eb
|
||||||
|
style D fill:#fef3c7,stroke:#f59e0b
|
||||||
|
style C fill:#fef3c7,stroke:#f59e0b
|
||||||
|
```
|
||||||
|
|
||||||
|
换句话说——对于 `triangle[i][j]`(既不是第一个也不是最后一个):
|
||||||
|
|
||||||
|
- **左上方** → 上一行、前一列 → `triangle[i-1][j-1]`
|
||||||
|
- **右上方** → 上一行、同一列 → `triangle[i-1][j]`
|
||||||
|
|
||||||
|
为什么不会越界?因为第 `i` 行有 `i+1` 个元素(下标 `0..i`),而上一行有 `i` 个元素(下标 `0..i-1`)。当你在第 `i` 行的中间位置取时,`j-1` 最小是 0(不会小于 0),`j` 最大是 `i-1`(不会超出上一行的范围)。
|
||||||
|
|
||||||
|
### 第三步:自底向上逐行构造
|
||||||
|
|
||||||
|
以 `numRows = 5` 为例,逐步填充:
|
||||||
|
|
||||||
|
| 步骤 | 当前行 | 操作 | 结果 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| 1 | 第 0 行 | 只有一个元素,直接放 1 | `[1]` |
|
||||||
|
| 2 | 第 1 行 | 首尾为 1,无中间元素 | `[1, 1]` |
|
||||||
|
| 3 | 第 2 行 | 首尾为 1,中间 = `1+1` | `[1, 2, 1]` |
|
||||||
|
| 4 | 第 3 行 | 首尾为 1,中间 = `1+2`, `2+1` | `[1, 3, 3, 1]` |
|
||||||
|
| 5 | 第 4 行 | 首尾为 1,中间 = `1+3`, `3+3`, `3+1` | `[1, 4, 6, 4, 1]` |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph "逐行构造过程"
|
||||||
|
R0["第 0 行: [1]"] --> R1["第 1 行: [1, 1]"]
|
||||||
|
R1 --> R2["第 2 行: 首尾填 1<br/>中间 = 1+1=2"]
|
||||||
|
R2 --> R3["第 3 行: 首尾填 1<br/>中间 = 1+2=3, 2+1=3"]
|
||||||
|
R3 --> R4["第 4 行: 首尾填 1<br/>中间 = 1+3=4, 3+3=6, 3+1=4"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style R0 fill:#dbeafe,stroke:#2563eb
|
||||||
|
style R4 fill:#dcfce7,stroke:#16a34a
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码提示
|
||||||
|
|
||||||
|
在动手写代码之前,想一想这些关键决策点:
|
||||||
|
|
||||||
|
> [!question] 💡 思考 1
|
||||||
|
> Go 语言中如何声明"二维切片"(切片切片 `[][]int`)?需要先预分配外层长度,内层逐个分配吗?
|
||||||
|
|
||||||
|
答案是:外层可以用 `make([][]int, numRows)` 一次性创建;然后对每一行分别用 `make([]int, i+1)` 创建,最后再填充数值。两步完成,清晰且高效。
|
||||||
|
|
||||||
|
> [!question] 💡 思考 2
|
||||||
|
> 循环怎么写最简洁?需要区分"第一行"这种特殊case 吗?
|
||||||
|
|
||||||
|
不需要!只要先统一把每行的首尾赋值为 1,再只遍历中间元素做加法就行——边界条件天然处理了,无需分支判断。
|
||||||
|
|
||||||
|
## 技巧
|
||||||
|
|
||||||
|
### 利用对称性减少计算量
|
||||||
|
|
||||||
|
杨辉三角的每一行都是**回文**的。比如第 4 行 `[1, 4, 6, 4, 1]`,左右完全对称。这意味着:
|
||||||
|
|
||||||
|
- 计算一半就够了,另一半直接镜像过去
|
||||||
|
- 对于较大的 `numRows`,能减少约一半的计算
|
||||||
|
|
||||||
|
不过 `numRows ≤ 30` 的情况下,优化意义不大,完整写出更直观。
|
||||||
|
|
||||||
|
### 杨辉三角 ↔ 二项式系数
|
||||||
|
|
||||||
|
杨辉三角的第 `i` 行第 `j` 列恰好等于组合数 $C(i, j)$:
|
||||||
|
|
||||||
|
$$\text{triangle}[i][j] = C(i, j) = \frac{i!}{j!(i-j)!}$$
|
||||||
|
|
||||||
|
所以这也是一道**数学题**。直接用组合数公式可以一行一行算,但需要注意阶乘溢出问题(Go 中 `int` 到 30 行没问题,更大就需要 `math/big` 了)。
|
||||||
|
|
||||||
|
| 方法 | 时间 | 空间 | 评价 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 模拟递推(推荐) | O(numRows²) | O(numRows²) | 最简单直观,不需要额外知识 |
|
||||||
|
| 组合数公式 | O(numRows²) | O(numRows²) | 需要考虑溢出 |
|
||||||
|
| 单行滚动数组 | O(numRows²) | O(numRows) | 只需保存一行,但代码反而复杂 |
|
||||||
|
|
||||||
|
## 代码
|
||||||
|
|
||||||
|
### Go 语言实现
|
||||||
|
|
||||||
|
```go
|
||||||
|
// generate generates the first numRows rows of Pascal's triangle.
|
||||||
|
// Each element is the sum of the two elements directly above it.
|
||||||
|
func generate(numRows int) [][]int {
|
||||||
|
// 初始化二维切片:外层长度为 numRows
|
||||||
|
triangle := make([][]int, numRows)
|
||||||
|
|
||||||
|
for i := 0; i < numRows; i++ {
|
||||||
|
// 第 i 行有 i+1 个元素
|
||||||
|
triangle[i] = make([]int, i+1)
|
||||||
|
|
||||||
|
// 1. 首尾都设为 1(边界条件)
|
||||||
|
triangle[i][0] = 1
|
||||||
|
triangle[i][i] = 1
|
||||||
|
|
||||||
|
// 2. 填充中间元素
|
||||||
|
// 注意:当 i < 2 时,没有中间元素,循环不执行
|
||||||
|
for j := 1; j < i; j++ {
|
||||||
|
triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return triangle
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 执行过程演示(numRows = 5)
|
||||||
|
|
||||||
|
```
|
||||||
|
i=0: [1] ← 只有首=尾=1
|
||||||
|
i=1: [1, 1] ← 只有首=尾=1,无中间元素
|
||||||
|
i=2: [1, 1+1, 1] = [1, 2, 1] ← 中间 j=1: triangle[1][0]+triangle[1][1]
|
||||||
|
i=3: [1, 1+2, 2+1, 1] = [1, 3, 3, 1] ← 中间 j=1,2
|
||||||
|
i=4: [1, 1+3, 3+3, 3+1, 1] = [1, 4, 6, 4, 1] ← 中间 j=1,2,3
|
||||||
|
|
||||||
|
返回前 5 行 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 复杂度分析
|
||||||
|
|
||||||
|
- **时间复杂度:O(numRows²)** — 总共需要填充 `1 + 2 + 3 + ... + numRows = numRows(numRows+1)/2` 个元素
|
||||||
|
- **空间复杂度:O(numRows²)** — 返回值本身占用的空间(不计入则额外空间为 O(1))
|
||||||
|
|
||||||
|
## 举一反三
|
||||||
|
|
||||||
|
这道题是**数组模拟 + 状态转移**的第一道入门 DP,核心模式非常通用:
|
||||||
|
|
||||||
|
- [[81-爬楼梯]] — 同样是线性 DP,只不过这里的转移来自上一行的两个相邻位置
|
||||||
|
- 剑指 Offer II 098. 路径数量 — 网格中的路径计数,同样使用递推思想
|
||||||
|
- 杨辉三角 II(LeetCode 119)— 只需要第 k 行,可以用滚动数组将空间降到 O(k)
|
||||||
|
|
||||||
|
> [!summary] 📌 本节要点
|
||||||
|
> 1. 边界条件:每行的第一个和最后一个元素恒为 1
|
||||||
|
> 2. 状态转移:triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j]
|
||||||
|
> 3. 实现技巧:先建好结构,再统一填值——无需特判特殊情况
|
||||||
Reference in New Issue
Block a user