Files
leetcode-go/动态规划/82-杨辉三角.md
T

211 lines
7.1 KiB
Markdown
Raw Normal View History

2026-05-16 18:39:36 +08:00
---
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. 实现技巧:先建好结构,再统一填值——无需特判特殊情况