P5732 【深基5.习7】杨辉三角
P5732 【深基5.习7】杨辉三角
题目描述
给出 \(n(1\le n\le20)\),输出杨辉三角的前 \(n\) 行。
如果你不知道什么是杨辉三角,可以观察样例找找规律。
输入格式
无
输出格式
无
输入输出样例 #1
输入 #1
6
输出 #1
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
解题思路
方法一:二维数组递推(基础)
思想
杨辉三角递推关系:
- 边界:\(a_{i,0} = a_{i,i} = 1\)
- 中间:\(a_{i,j} = a_{i-1,j-1} + a_{i-1,j}\)
时间复杂度
\(O(n^2)\),空间复杂度 \(O(n^2)\)。
C++ 代码
#include <iostream>
using namespace std;
int a[25][25];
int main() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
a[i][0] = a[i][i] = 1;
for (int j = 1; j < i; ++j)
a[i][j] = a[i-1][j-1] + a[i-1][j];
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= i; ++j)
cout << a[i][j] << " \n"[j == i];
}
return 0;
}
说明
适合初学者理解杨辉三角的生成过程。当 \(n\) 较小时完全够用,但空间消耗较大。
方法二:滚动数组(空间优化)
思想
观察递推式:第 \(i\) 行的值只依赖于第 \(i-1\) 行,因此无需保存整个二维表。
用一维数组 row 保存当前行,从后往前更新,避免覆盖尚未使用的上一行值。
滚动数组也可用于优化DP问题的空间占用。
时间复杂度
\(O(n^2)\),空间复杂度 \(O(n)\)。
C++ 代码
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> row(n + 1, 0);
row[1] = 1; // 第一行
for (int i = 1; i <= n; ++i) {
// 从后往前更新,保证 row[j-1] 还是上一行的数据
for (int j = i; j >= 1; --j) {
row[j] = row[j] + row[j-1];
cout << row[j] << " \n"[j == i];
}
}
return 0;
}
细节
- 必须逆序更新,若顺序更新,
row[j]会用本行已改变的值覆盖row[j-1],导致错误。 - 因为每行对称,逆序输出依然正确。
适用场景
\(n\) 稍大但仍可接受 \(O(n^2)\) 时间时,可大幅节省内存。也是动态规划中空间优化的经典技巧。
方法三:组合数递推公式(数学法)
思想
杨辉三角第 \(n\) 行第 \(k\) 个数(从 0 开始计数)等于组合数 \(C(n,k)\)。
利用公式逐项递推:
不必保存整行,直接生成并输出。
时间复杂度
求每个数 \(O(1)\),总时间复杂度 \(O(n^2)\),空间复杂度 \(O(1)\)(不计输出)。
C++ 代码
#include <iostream>
using namespace std;
int C(int n, int k) {
if (k == 0) return 1;
return C(n, k-1) * (n - k + 1) / k;
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= i; ++j)
cout << C(i, j) << " \n"[j == i];
}
return 0;
}
注意
- 一定要区分行列的起止编号:第 \(i\) 行对应 \(C(i, j)\)(\(j\) 从 0 到 \(i\))。
- 递归式是线性递推,但若重复调用 \(C(n,k)\) 会产生大量重复计算(时间复杂度升为 \(O(n^3)\))。
改进法:① 改为迭代递推;② 记忆化存储。但本题 \(n\le20\) 递归也能通过。 - 若 \(n\) 更大,中间结果可能超出
int范围,需要取模处理。
方法四:拓展——大数取模与卢卡斯定理
当 \(n\) 较大(如 \(n\le 10^5\))且需要输出对某个质数 \(p\) 取模的结果时,前三种方法都会遇到问题:
- 数值溢出(组合数爆炸增长)
- \(O(n^2)\) 时间复杂度过高
解决方案
4.1 预计算阶乘与逆元(\(O(n)\) 预处理,\(O(1)\) 查询)
先用 \(O(n)\) 计算 \(1!\) 到 \(n!\) 及其逆元,之后组合数直接用公式:
(要求 \(p\) 为质数,用费马小定理求逆元)
4.2 卢卡斯定理
当 \(n\) 极大(如 \(10^{18}\))但 \(p\) 较小且为质数时,可用卢卡斯定理:
将问题规模缩小至 \(p\) 范围内,递归或递推均可。
本题数据范围较小,不需要上述高阶技巧,但它们是全国/国际赛事中“组合数取模”类题目的核心知识点,值得提前了解。
技巧总结
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 二维数组递推 | \(O(n^2)\) | \(O(n^2)\) | \(n\) 小,需保留整个三角形 |
| 滚动数组 | \(O(n^2)\) | \(O(n)\) | \(n\) 中等,节省空间 |
| 组合数公式 | \(O(n^2)\) | \(O(1)\) | \(n\) 小,只输出数,极简实现 |
| 阶乘逆元 | \(O(n)\) / \(O(1)\) 查询 | \(O(n)\) | \(n\) 大,需取模 |
| 卢卡斯定理 | \(O(\log_p n)\) 单次 | 视实现 | \(n\) 极大,\(p\) 为小质数 |
输出格式技巧
cout << val << " \n"[j == i];
" \n" 是一个字符串常量,[j==i] 利用布尔值转 0/1 选择空格或换行。
竞赛中可简化代码,但非必须,不影响算法本质。
建议学习路径
先掌握方法一(理解递推),再优化至方法二(学会滚动数组思想)。
若有余力,研究方法三(组合数学视角)和方法四(取模问题),为更复杂的组合数学题目打下基础。
下面是Python代码实现版本:
方法一:二维数组递推(基础)
n = int(input())
a = [[0] * (n + 1) for _ in range(n)]
for i in range(n):
a[i][0] = a[i][i] = 1
for j in range(1, i):
a[i][j] = a[i-1][j-1] + a[i-1][j]
for i in range(n):
print(' '.join(str(a[i][j]) for j in range(i + 1)))
说明
- 列表推导式生成二维表,先设边界,再递推。
- 输出用
join拼接,简洁清晰,是 Python 推荐方式。
方法二:滚动数组(空间优化)
n = int(input())
row = [0] * (n + 1)
row[1] = 1 # 第一行 [1]
for i in range(1, n + 1):
# 从后往前更新
for j in range(i, 0, -1):
row[j] = row[j] + row[j - 1]
print(row[j], end=' ' if j > 1 else '\n')
说明
- 用列表
row模拟一维滚动数组,逆序更新避免覆盖。 - 输出控制:行内数字用空格分隔,行末换行。
方法三:组合数公式(数学法)
def C(n, k):
if k == 0:
return 1
return C(n, k - 1) * (n - k + 1) // k
n = int(input())
for i in range(n):
print(' '.join(str(C(i, j)) for j in range(i + 1)))
说明
- 递归实现组合数,注意用整除
//保证整数结果。 - 由于 Python 自带大整数,不会像 C++ 那样溢出,但当
n较大时递归深度和重复计算会成为瓶颈(本题n≤20完全无碍)。
优化变体:迭代递推
def C_iter(n, k):
res = 1
for i in range(1, k + 1):
res = res * (n - i + 1) // i
return res
方法四:拓展——阶乘逆元取模(Python 版)
当 n 较大且要求取模时(假设模数为质数 MOD = 10**9 + 7):
MOD = 10**9 + 7
MAXN = 100000 # 根据需要调整
# 预计算阶乘与逆元
fact = [1] * (MAXN + 1)
inv_fact = [1] * (MAXN + 1)
for i in range(1, MAXN + 1):
fact[i] = fact[i-1] * i % MOD
# 费马小定理求逆元
inv_fact[MAXN] = pow(fact[MAXN], MOD - 2, MOD)
for i in range(MAXN, 0, -1):
inv_fact[i-1] = inv_fact[i] * i % MOD
def C_mod(n, k):
if k < 0 or k > n:
return 0
return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD
说明
- Python 的
pow(a, b, mod)自带快速幂取模,可直接求逆元。 - 查询组合数只需 O(1),适合大规模输出。
输出技巧对比
C++ 的 " \n"[j==i] 在 Python 中天然对应 ' ' if j < i else '\n',但用 ' '.join() 可读性更佳:
# 逐行输出
for i in range(n):
print(' '.join(str(comb(i, j)) for j in range(i + 1)))

浙公网安备 33010602011771号