--- 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
中间 = 1+1=2"] R2 --> R3["第 3 行: 首尾填 1
中间 = 1+2=3, 2+1=3"] R3 --> R4["第 4 行: 首尾填 1
中间 = 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. 实现技巧:先建好结构,再统一填值——无需特判特殊情况