组合数学
三件事讲清楚:① 该用加法还是乘法数数?② 排列和组合差在哪?③ 杨辉三角为什么就是组合数。
一、加法原理 vs 乘法原理
同样是“早餐”——主食有 🥟包子 / 🍜面条 / 🥣粥(3 种),饮料有 🥛牛奶 / 🧋豆浆(2 种)。先点下面两个按钮,看两种数法有什么不同。
点上面任意一个按钮看看。
口诀:分类(任选一类就办完)用加法,关键词“或”;分步(每步都要做)用乘法,关键词“先…再…”。
二、排列 vs 组合(顺序重不重要?)
从 n 个人里挑 m 个。排队照相讲顺序 → 排列 A(n,m)=n!/(n-m)!;分成一组不讲顺序 → 组合 C(n,m)=A(n,m)/m!。点一个“组合”看它能排出几种队。
10
组合 C(n,m) · 几组
6
× m! · 每组排法
60
= 排列 A(n,m)
点一个下面的“组合”,看它能排成几种不同的队(A = C × m!)。
同样“5 选 3”:排成一队是 A=60,分成一组是 C=10,正好差 3! = 6 倍——因为每组都能排出 3! 种队。
三、杨辉三角 = 组合数表
第 n 行第 m 个数(都从 0 数)就是 C(n,m)。每个数 = 它头顶上两个数之和。点任意一个数看它从哪两个数加出来。
点一个数试试。蓝色 = 它头顶的两个“爸爸”,绿色 = 它的对称兄弟。
看出来了吗:左右对称(C(n,m)=C(n,n-m));每行加起来正好是 2ⁿ(每个元素“选或不选”)。
四、写成代码(C++14)
求一批组合数,最稳的写法就是把杨辉三角“递推”出来——只用加法,不碰除法和大阶乘。
const int N = 2005;
long long C[N][N];
void initC() {
for (int i = 0; i < N; i++) {
C[i][0] = 1; // 每行第 0 个都是 1
for (int j = 1; j <= i; j++)
C[i][j] = C[i-1][j-1] + C[i-1][j]; // 帕斯卡公式:头顶两数相加
}
}
⚠️ 会溢出:组合数长得飞快,C(67,33) 就超出 long long 了。所以题目要么把 n 限得很小,要么让你取模——那就把加法改成 (C[i-1][j-1] + C[i-1][j]) % MOD。例题见 P2822 组合数问题(杨辉三角递推 + 二维前缀和)。
小结
① 先判分类还是分步
分类 → 加法(或);分步 → 乘法(先…再…)。综合题常常外层加、内层乘。
分类 → 加法(或);分步 → 乘法(先…再…)。综合题常常外层加、内层乘。
② 排列还是组合
讲顺序 → 排列 A=n!/(n−m)!;不讲顺序 → 组合 C=n!/(m!(n−m)!)。差一个 m!。
讲顺序 → 排列 A=n!/(n−m)!;不讲顺序 → 组合 C=n!/(m!(n−m)!)。差一个 m!。
③ 杨辉三角
第 n 行第 m 个 = C(n,m);每数 = 头顶两数之和;左右对称;行和 = 2ⁿ。
第 n 行第 m 个 = C(n,m);每数 = 头顶两数之和;左右对称;行和 = 2ⁿ。
④ 编程
首选递推(只加法,按需取模),警惕溢出。配套 前缀和/ST 表 收尾 P2822。
首选递推(只加法,按需取模),警惕溢出。配套 前缀和/ST 表 收尾 P2822。