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)\)
利用公式逐项递推:

\[C(n,0) = 1,\quad C(n,k) = \frac{C(n,k-1) \cdot (n-k+1)}{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!\) 及其逆元,之后组合数直接用公式:

\[C(n,k) \equiv n! \cdot (k!)^{-1} \cdot ((n-k)!)^{-1} \pmod{p} \]

(要求 \(p\) 为质数,用费马小定理求逆元)

4.2 卢卡斯定理

\(n\) 极大(如 \(10^{18}\))但 \(p\) 较小且为质数时,可用卢卡斯定理:

\[C(n,k) \equiv C(n/p,\; k/p) \cdot C(n\bmod p,\; k\bmod p) \pmod{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)))
posted @ 2026-05-09 00:05  炸酱面配小米酒  阅读(43)  评论(0)    收藏  举报