组合数学复习笔记
stripe_python · · 算法·理论
加法 & 乘法原理
- 加法原理:有
n 类方法,a_i 为第i 类中方法的数目,则总方法数为\sum a_i 。 - 乘法原理:有
n 个步骤,a_i 为第i 步中方法的数目,则总方法数为\prod a_i 。
这两者的区别是:加法分类,乘法分步。
排列组合
排列
从
我们这样计算:第一个数有
规定
而如果
全排列的枚举
在 C++ 标准库中提供了 next_permutation 函数,可以这样使用:
// a为要枚举排列的数组,n为长度
sort(a + 1, a + n + 1);
do {
...
} while (next_permutation(a + 1, a + n + 1));
时间复杂度为
有重复元素的排列问题
顾名思义就是元素有重,此时需要除去重复的排列。
考虑一组有
其中
圆排列
从
组合
从
考虑先作排列,由于集合无序,需要除去重复的组合,也就是
规定
组合的枚举
这是 pj 组题目,代码如下:
int n, m, cur[N];
void dfs(int x) {
if (x > m) return ..., void(); // 这里cur就是一个组合,进行处理
for (int i = cur[x - 1] + 1; i <= n; i++) cur[x] = i, dfs(x + 1);
}
dfs(1);
它用于枚举 next_permutation,还能实现排列的枚举。
二项式定理
这个定理表明,二项式
我们在初中阶段学习过杨辉三角:
其每一个位置的数字通过左上 + 右上确定,每一行所对应的就是二项式展开的系数。
组合数的性质
- 对称性:
代数推导易证,组合意义就是把选出的集合取补集。
- 递推式:
代数推导略去,组合意义类似 dp 的思想可以证明。这个式子实际上就是杨辉三角的递推式。
- 二项式定理的特殊情况:
也就是杨辉三角每一行的和。
- 斐波那契数列:
把杨辉三角每条斜
- 范德蒙恒等式:
假设有两堆物品,每堆分别有
计算组合数
定义法
- 优点:写起来简单,不用预处理。
- 缺点:查询复杂度
O(m) 。
根据定义直接计算,注意先乘后除。
inline long long C(int n, int m) {
if (m > n) return 0;
long long res = 1;
for (int i = 1; i <= m; i++) {
res = res * (n - i + 1) % mod * power(i, mod - 2) % mod;
}
return res;
}
递推法
- 优点:任意模数,写起来简单,查询
O(1) 。 - 缺点:预处理时空复杂度均为
O(n^2) 。
即利用组合数性质 2 预处理杨辉三角。
for (int i = 0; i < N; i++) c[i][0] = 1, c[i][i] = 1;
for (int i = 0; i < N; i++) {
for (int j = 1; j < i; j++) c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
}
逆元法
- 优点:预处理
O(n) ,查询O(1) 。 - 缺点:只能在模数为质数时使用。
通过预处理阶乘和阶乘的逆元,直接用定义式计算组合数。
inline long long power(long long a, long long b) {
long long res = 1;
for (a %= mod; b; b >>= 1) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
}
return res;
}
long long fac[N], ifac[N];
inline void pre() {
fac[0] = fac[1] = ifac[0] = ifac[1] = 1;
for (int i = 2; i < N; i++) fac[i] = fac[i - 1] * i % mod, ifac[i] = power(fac[i], mod - 2);
}
inline long long C(int n, int m) {
if (n < m) return 0;
return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
Lucas 定理法
- 优点:预处理
O(p) ,查询O(\log n) ,适用于模数小而n,m 很大的情况。 - 缺点:码量稍大,只能处理
p 为质数的情况。
Lucas 定理如下:
其中
inline long long power(long long a, long long b) {
long long res = 1;
for (a %= mod; b; b >>= 1) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
}
return res;
}
long long fac[N], ifac[N];
inline void pre() {
fac[0] = fac[1] = ifac[0] = ifac[1] = 1;
for (int i = 2; i < N; i++) fac[i] = fac[i - 1] * i % mod, ifac[i] = power(fac[i], mod - 2);
}
inline long long C(int n, int m) {
if (n < m) return 0;
return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
long long lucas(long long n, long long m) {
if (m == 0) return 1;
if (n < mod && m < mod) return C(n, m);
return lucas(n / mod, m / mod) * C(n % mod, m % mod) % mod;
}
P3807 【模板】卢卡斯定理/Lucas 定理。
exLucas 法
- 优点:任意模数,预处理约为
O(p \log p) ,查询O(\log p) ,同样适用于模数小而n,m 很大的情况。 - 缺点:码量很大。
exLucas 跟 Lucas 没有半毛钱关系。
将模数
现在的重点是算
For example:
至于阶乘逆元,用 exGCD 求得即可。
inline long long power(long long a, long long b, long long p) {
long long res = 1;
for (a %= p; b; b >>= 1) {
if (b & 1) res = res * a % p;
a = a * a % p;
}
return res;
}
void exgcd(long long a, long long b, long long& x, long long& y) {
if (b == 0) return x = 1, y = 0, void();
exgcd(b, a % b, x, y);
int t = x;
x = y, y = t - a / b * y;
}
inline long long inverse(long long x, long long p) {
long long a, b;
exgcd(x, p, a, b);
return (a % p + p) % p;
}
inline long long calc(long long n, long long x, long long p) {
if (n == 0) return 1;
long long s = 1;
for (long long i = 1; i <= p; i++) {
if (i % x) s = s * i % p;
}
s = power(s, n / p, p);
for (long long i = n / p * p + 1; i <= n; i++) {
if (i % x) s = i % p * s % p;
}
return s * calc(n / x, x, p) % p;
}
inline long long multilucas(long long m, long long n, long long x, long long p) {
int cnt = 0;
for (long long i = m; i; i /= x) cnt += i / x;
for (long long i = n; i; i /= x) cnt -= i / x;
for (long long i = m - n; i; i /= x) cnt -= i / x;
return power(x, cnt, p) % p * calc(m, x, p) % p * inverse(calc(n, x, p), p) % p
* inverse(calc(m - n, x, p), p) % p;
}
long long x[20];
inline int crt(int n, long long* a, long long* m) {
long long mod = 1, res = 0;
for (int i = 1; i <= n; i++) mod *= m[i];
for (int i = 1; i <= n; i++) {
x[i] = mod / m[i];
long long x0 = -1, y0 = -1;
exgcd(x[i], m[i], x0, y0);
res += a[i] * x[i] * (x0 >= 0 ? x0 : x0 + m[i]);
}
return res % mod;
}
inline long long exlucas(long long m, long long n, long long p) {
int len = 0;
long long p0[20], a0[20];
for (long long i = 2; i * i <= p; i++) {
if (p % i) continue;
p0[++len] = 1;
while (p % i == 0) p0[len] *= i, p /= i;
a0[len] = multilucas(m, n, i, p0[len]);
}
if (p > 1) p0[++len] = p, a0[len] = multilucas(m, n, p, p);
return crt(len, a0, p0);
}
P4720 【模板】扩展卢卡斯定理/exLucas。
经典模型
捆绑法
Q: 有
A: 将
插空法
Q: 有
A: 先将
插板法
Q1: 现有
(可以抽象为:求方程
A1: 不考虑分组,而是考虑间断点,将
Q2: 现有
(可以抽象为:求方程
A2: 先借
Q3: 现有
(可以抽象为:求方程
A3: 先借
错位排列
错排数
例如,
计算错排数可以递推。考虑
- 前面
n-1 个数已经是错位排列; - 前面
n-1 个数有一个在原位上,其他错位。
对于情况 1,第
对于情况 2,第
因此,
另外地,
long long D[N];
inline void calc() {
D[1] = 0, D[2] = 1;
for (int i = 3; i < N; i++) D[i] = (i - 1) * (D[i - 1] + D[i - 2]);
}
板子题为 P1595 信封问题。
Catalan 数
Catalan 数的起源是凸多边形三角剖分问题,即对于一个凸
解决问题
- 括号序列计数
Q: 计算
A: 方案数为 (,设之后有一个长度为 ),再然后是另一个子序列,长度为
- 不同构二叉树计数
Q: 计算
A: 数目为
- 三角剖分问题
Q: 求对于一个凸
A: 方案数为
- 不越过对角线的网格路径
Q: 在
A: 路径数为
- 出栈序列计数
Q: 入栈序列为一个
A: 数目为
由此可见,能用 Catalan 数解决的问题都满足一个递归分解结构:一个大小为
计算公式
上面已经给出了一个递推式:
还有一个递推式:
通项公式:
使用递推式可以在
Stirling 数
第二类 Stirling 数
第二类 Stirling 数
第二类 Stirling 数有递推计算和通项计算两种计算方法。
递推式
考虑到第
- 将第
n 个元素单独放一个新子集,方案数为S(n-1,k-1) ; - 将第
n 个元素放一个已有子集,方案数为k \times S(n-1,k) 。
由加法原理得:
通项公式
证明不会。
第一类 Stirling 数
第一类 Stirling 数
第一类 Stirling 数可通过递推计算。考虑到第
- 将第
n 个元素单独放一个新环,方案数为s(n-1,k-1) ; - 将第
n 个元素放一个已有环,方案数为(n-1) \times s(n-1,k) 。
由加法原理得:
Bell 数
Bell 数
{1},{2},{3}
{1,2},{3}
{1,3},{2}
{1},{2,3}
{1,2,3}
所以
递推式
设
当它和
仿照杨辉三角,可以构造一个贝尔三角形:
此时
int b[N][N];
void pre() {
b[0][0] = 1;
for (int i = 1; i < N; i++) {
b[i][0] = b[i - 1][i - 1];
for (int j = 1; j <= i; j++) b[i][j] = b[i - 1][j - 1] + b[i][j - 1];
}
}
与第二类 Stirling 数的关系
枚举划分成
这表明:Bell 数
Eulerian 数
在组合数学中,Eulerian 数
Eulerian 数可通过递推计算。考虑第
- 将它插入到一个上升位置中或排列之前,没有贡献;
- 从
A(n-1,m-1) 转移时,不能满足条件 1,共有n-(m-1)=n-m 种方案,且贡献为1 ; - 从
A(n-1,m) 转移时,必须满足条件 1 才有贡献,方案数为m+1 。
因此:
边界为
代码实现:
int A(int n, int m) {
if (m >= n || n == 0) return 0;
if (m == 0) return 1;
return (n - m) * A(n - 1, m - 1) + (m + 1) * A(n - 1, m);
}