【组合数学】基础知识 学习笔记
组合数学与组合计数
计数原理
分类加法计数原理:做一件事,有多类方法,则总的方法数是所有类方法数之和。
分步乘法计数原理:做一件事,需要多步完成,则总的方法数是所有步方法的乘积。
例题:P3197 [HNOI2008] 越狱
排列与组合
排列数:从 \(n\) 个数中选出 \(m\) 个数排成一列,所有方案数根据乘法原理,有 $$P^m_n = n \times (n-1) \times (n-2) \times \dots \times (n-m+1) = \frac{n!}{(n-m)!}$$
全排列:\(P_n^n = n!\)
组合数:从 \(n\) 个数中选出 \(m\) 个数,不考虑相对顺序,则与排列数有以下关系:$$C^m_n = \frac{A^m_n}{m!} = \frac{n!}{(n-m)!m!}$$
组合数的性质:
- \(C^m_n = C^{n-m}_n\)
- \(C^m_n = C^m_{n-1} + C^{m-1}_{n-1}\) (杨辉三角)
- \(\sum_{i=0}^n C_n^i = 2^n\)
求组合数的代码实现
- 递推法。根据性质二,可以以 \(n\) 为阶段递推出组合数,时间复杂度 \(O(nm)\) 可以求出所有 \(n\) 内的组合数。
- 逆元法。若计算 \(C^n_m\) 在取模 \(p\) 意义下的值,可以在 \(n \log p\) 的时间复杂度内用费马小定理求逆元的方法同时预处理出阶乘在取模 \(p\) 意义下的值和逆元,然后在模意义下运算即可。
二项式定理
牛顿二项式定理由艾萨克·牛顿于1664年、1665年间提出:
\[(a+b)^n = \sum_{i=0}^n \binom{i}{n}a^ib^{n-i}
\]
其中,\(\binom{i}{n}\) 表示二项式系数,数值上等于 \(C_n^i\),是数学上常用的组合数表示方法。
例题:[NOIP2011 提高组] 计算系数 | 洛谷 P1313
根据二项式定理,有 \((by+ax)^k = \sum_{i=0}^{k} \binom{i}{k} b^iy^ia^{k-i}x^{k-i}\),当 \(i = m\) 时,对应项的系数 \(\binom{m}{k}a^nb^m\) 即为所求。按照上述方法计算即可,代码实现如下:
#include <bits/stdc++.h>
using namespace std;
// #define int long long
const int mod = 10007, N = 1e6 + 5;
int a, b, k, n, m;
int qmi(int x, int p) // x^p % mod
{
x %= mod;
if (x == 1)
return (1 % mod);
int res = 1;
while (p)
{
if (p & 1)
res = res * x % mod;
x = x * x % mod;
p >>= 1;
}
return res;
}
int jc[N], jc_inv[N];
int C(int n, int m) // C^m_n
{
return jc[n] * jc_inv[m] % mod * jc_inv[n - m] % mod;
}
signed main()
{
ios::sync_with_stdio(false);
#ifdef DEBUG
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif
// Don't stop. Don't hide. Follow the light, and you'll find tomorrow.
cin >> a >> b >> k >> n >> m;
jc[0] = jc_inv[0] = 1;
for (int i = 1; i <= k; i++)
{
jc[i] = jc[i - 1] * i % mod;
jc_inv[i] = jc_inv[i - 1] * qmi(i, mod - 2) % mod;
}
cout << C(k, m) * qmi(a, n) % mod * qmi(b, m) % mod << endl;
return 0;
}