(3) 杨辉三角与组合数
GESP C++ 八级考试大纲知识点梳理系列文章:
- 计数原理:加法与乘法
- 排列与组合
- 杨辉三角与组合数
- 倍增法
- 代数与平面几何 {: .prompt-tip}
继上一篇我们探讨了排列和组合之后,GESP C++八级大纲的第三条考点非常经典,它是计算机算法(尤其是动态规划)的重要入门案例。
(3)掌握杨辉三角形(又称帕斯卡三角形)的概念。 {: .prompt-info}
杨辉三角形(Yang Hui’s Triangle),在西方称为帕斯卡三角形(Pascal’s Triangle),是一个无限对称的数字三角形。它不仅在形式上优美,更蕴含了深厚的组合数学原理。
本人也是边学、边实验、边总结,且对考纲深度和广度的把握属于个人理解。因此本文更多的不是一个教程,而是个人知识梳理,如有遗漏、疏忽,欢迎指正、交流。 {: .prompt-warning}
一、杨辉三角的概念与构造
杨辉三角的构造规则非常简单,概括为一句话:每个数等于它上方两数之和。
1.1 形状演示
如果我们把三角形写出来,前几行是这样的:
Row 0: 1
Row 1: 1 1
Row 2: 1 2 1
Row 3: 1 3 3 1
Row 4: 1 4 6 4 1
...或者用左对齐的方式(在计算机二维数组中更常见):
1
1 1
1 2 1
1 3 3 1
1 4 6 4 11.2 递推公式
如果用 表示第 行第 列的数字(行列索引从 0 开始),那么有:
- 边界条件:每一行的第一个数和最后一个数都是 1。
- 递推关系:中间的数等于它正上方 () 和左上方 () 的数之和。
这其实就是我们上一章提到的组合数公式 的完美图形化体现。
二、杨辉三角与组合数的关系
杨辉三角最核心的数学意义在于:第 行的第 个数,恰好就等于组合数 (即 )。
注意:这里的行号 和列号 都是从 0 开始计数的。
2.1 验证
- 第 2 行:1, 2, 1
- 第 3 行:1, 3, 3, 1
- 第 4 行:1, 4, 6, 4, 1
- ,确实对应第 4 行第 2 列的 6。
2.2 为什么会有这个关系?
这就回到了组合数的定义。 假设我们要在 个物品中选 个(): 我们可以把这 个物品中的某一个特定物品(比如“特殊的红球”)单独拿出来看。 选出的 个物品中,只有两种情况:
- 包含这个红球:那么还需要从剩下的 个中选 个。即 。
- 不包含这个红球:那么需要从剩下的 个中选完整 个。即 。
所以,。这正是杨辉三角的递推公式。
三、杨辉三角的应用
3.1 二项式定理系数
杨辉三角的第 行,正是二项式 展开后的各项系数。
- 1, 1
- 1, 2, 1
- 1, 3, 3, 1
在解题时,如果让你求 的展开式系数,可以直接打印杨辉三角。
3.1.1 扩展:关于“第 行的所有数之和为 ”
这一条的原理主要有两种解释方式:代数推导和组合意义。
(1) 代数推导(利用二项式定理) 我们在文章前面提到过,杨辉三角的第 行实际上就是 展开后的系数。
如果我们令 且 ,代入上面的公式,就会神奇地发现:
即:
而 正是杨辉三角第 行的所有数字。所以,它们的和一定等于 。
(2) 组合意义(子集总数)
想象你有一个包含 个不同元素的集合(比如 )。
表示选 0 个元素的方案数(空集); 表示选 1 个元素的方案数; … 表示选 个元素的方案数(全集)。 把这一行所有数字加起来,意思就是**“从中选出任意个元素的方案总数”,也就是这个集合的所有子集的数量。 对于集合中的每一个元素,它只有两种状态:“被选中”** 或 “不被选中”。 个元素,每个都有 2 种选择,根据乘法原理,总方案数就是 。
3.2 动态规划求组合数
这是编程中最实用的功能。 直接利用阶乘公式 计算组合数有两个大问题:
- 溢出风险: 就已经超过
long long范围了,但 其实并不大。 - 计算量:频繁计算阶乘效率低,且涉及除法(取模时需要逆元)。
利用杨辉三角递推(其实就是动态规划),我们可以只用加法,安全、快速地计算出小范围内的所有组合数。
适用于 左右的情况。如果是 很大但 很小,或者 都很大且需要取模,则需要用其他方法(如卢卡斯定理)。
四、代码实现 (C++)
下面展示如何利用二维数组生成杨辉三角,并查询组合数。
#include <iostream>
#include <vector>
using namespace std;
// 定义最大行数
const int MAXN = 20;
long long triangle[MAXN + 1][MAXN + 1];
void buildPascalTriangle() {
// 初始化边界和递推
for (int i = 0; i <= MAXN; i++) {
triangle[i][0] = 1; // 每行第一个是 1
triangle[i][i] = 1; // 每行最后一个是 1
for (int j = 1; j < i; j++) {
// 递推公式:头顶两数之和
triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j];
}
}
}
void printTriangle() {
for (int i = 0; i <= 10; i++) { // 打印前10行看看
// 打印一些空格做格式化(仅仅为了美观,非必须)
for(int k=0; k<10-i; k++) cout << " ";
for (int j = 0; j <= i; j++) {
printf("%4lld", triangle[i][j]);
}
cout << endl;
}
}
int main() {
buildPascalTriangle();
// 1. 打印图形
cout << "=== 杨辉三角前10行 ===" << endl;
printTriangle();
// 2. 利用杨辉三角查询组合数
// C(5, 2)
int n = 5, m = 2;
cout << "\n=== 组合数查询 ===" << endl;
cout << "C(" << n << ", " << m << ") = " << triangle[n][m] << endl;
// C(10, 4)
n = 10; m = 4;
cout << "C(" << n << ", " << m << ") = " << triangle[n][m] << endl;
return 0;
}代码运行结果演示
=== 杨辉三角前10行 ===
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
1 9 36 84 126 126 84 36 9 1
=== 组合数查询 ===
C(5, 2) = 10
C(9, 4) = 126五、实战演练:计算系数(洛谷 P1313)
题目描述: 给定一个多项式 ,请求出多项式展开后 项的系数。结果对 取模。
核心考点: 二项式定理 + 组合数计算
解题思路: 根据二项式定理, 的通项公式为:
题目要求 项的系数,意味着对应的是 这一项。 即 且 (题目保证 )。 所以,该项的系数为:
为何使用杨辉三角? 题目中 。虽然 10007 是质数,可以用卢卡斯定理或逆元,但直接用杨辉三角预处理 是最稳健、最不容易出错的方法(此范围 约为 ,非常快)。
代码核心片段:
// 1. 预处理杨辉三角 (只算到 k 行即可)
for (int i = 0; i <= k; i++) {
c[i][0] = 1;
c[i][i] = 1;
for (int j = 1; j < i; j++)
c[i][j] = (c[i-1][j-1] + c[i-1][j]) % 10007; // 边算边取模,完美
}
// 2. 计算答案
// ans = C(k, m) * a^n * b^m % 10007
long long ans = c[k][m];
ans = (ans * quick_pow(a, n)) % 10007; // 配合快速幂计算 a^n
ans = (ans * quick_pow(b, m)) % 10007; // 配合快速幂计算 b^m
cout << ans << endl;代码核心片段分析(以连乘取模为例)
这段代码的核心逻辑是利用模运算的乘法性质来防止数据溢出。
我们以 3 个数连乘取模为例,公式如下:
即:步步取模。每乘一个数,就取一次模,最终结果不变。
1. 变量对应关系
在代码中,我们需要计算的是:
我们可以将其视为三个数 的连乘:
- (即
c[k][m]) - (即
quick_pow(a, n)) - (即
quick_pow(b, m))
2. 数学推导与代码执行对照
假设我们用小一点的数字来模拟: 计算 。 直接计算:, 。结果为 4。
代码的分步执行过程(步步取模):
初始状态:
long long ans = c[k][m]; // 对应 A假设 ,则
ans = 2。第一步乘法:
ans = (ans * quick_pow(a, n)) % 10007; // 对应 (A * B) % P假设 。
- 计算乘积:
- 取模:
- 此时
ans = 1。
第二步乘法:
ans = (ans * quick_pow(b, m)) % 10007; // 对应 (ans * C) % P这里用的是上一步计算出的
ans(即 1)。假设 。- 计算乘积:
- 取模:
- 此时
ans = 4。
结论:最终结果 4 与直接计算的结果一致。
3. 为什么要这样做?
虽然本题模数 很小,直接算也不会溢出 long long,但在算法竞赛中,通常模数 很大(如 )。
如果不“步步取模”,直接算 ,结果可能会是一个巨大的天文数字,远超计算机整数类型的存储上限(即溢出)。通过步步取模,我们保证了中间结果永远不会超过 (约 范围内),从而可以安全地使用 long long 存储。
六、总结
杨辉三角形是连接几何(三角形结构)与代数(组合数、二项式定理)的桥梁。在 GESP 八级考试中,主要考察以下几点:
- 基本性质:要知道第 行第 列对应 。
- 递推构造:能够手写代码构建杨辉三角数组(DP思想)。
- 行和性质:第 行的所有数之和为 (对应集合所有子集的总数)。
- 组合数计算:利用杨辉三角解决不需要取模或模数不是质数的组合数计算问题。
注:关于“不需要取模或模数不是质数时的组合数计算”
这一条是针对编程竞赛(算法竞赛)中常见的坑点。
为什么要用杨辉三角(递推法)? 通常计算组合数 有两种方法:
公式法: 递推法(杨辉三角): 公式法的问题在于那个除法。
在计算机中,整数运算 不等于 。 要计算模运算下的除法,必须转换成 乘以 的逆元,即 。 关键限制:只有当模数 是质数(且 不是 的倍数)时,我们可以简单地用费马小定理求逆元。 如果题目要求的模数 不是质数(比如 ),或者根本不取模(但在 long long 范围内),公式法就非常麻烦:
不取模时:中间的阶乘 极易溢出(21! 就爆了 unsigned long long),通过公式算出结果很难。 模数组合数非质数时:无法使用费马小定理求逆元,需要用扩展欧几里得算法求逆元,或者分解质因数,代码极其复杂且容易写错。 杨辉三角的优势: 它全程只使用加法()。
没有除法:不需要担心除不尽,也不需要求逆元。 适用性广:无论模数 是质数还是合数,加法取模恒成立
。 防溢出:可以直接在计算过程中取模,或者直接利用 long long 存储最终结果(只要最终结果不超)。 总结:只要 的范围较小(比如 ),无论模数是什么妖魔鬼怪,这套杨辉三角递推代码都是通杀的“万金油”解法。
掌握了杨辉三角,我们就有了处理简单组合计数问题的有力武器。
本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
“luogu-”系列题目可在 洛谷题库 在线评测。
“bcqm-”系列题目可在 编程启蒙题库 在线评测。
GESP/CSP认证交流QQ群: 688906745
