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

211 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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. 实现技巧:先建好结构,再统一填值——无需特判特殊情况