组合数学习笔记
定义
组合数通常写作 \(C_n^m\), 表示从 \(n\) 个数中选 \(m\) 个数的方法数。
怎么算
1.杨辉三角
这个东西是一个递推式,如下:
这样我们可以去递推或者递归去求一个组合数,时间复杂度是 \(O(nm)\)。
这个东西其实很好理解,有点类似动态规划的思想,我们把 \(C_n^m\) 分成选第 \(n\) 个和不选第 \(n\) 个元素。若选第 \(n\) 个数,则需要在前 \(n-1\) 个元素中选 \(m-1\) ,所以方法数是 \(C_{n-1}^{m-1}\);若不选第 \(n\) 个数,则需要在前 \(n-1\) 个数中选 \(m\) 个数,方法数为 \(C_{n-1}^m\)。所以 \(C_n^m = C_{n-1}^{m-1} + C_{n-1}^m\)。
code
int cmb(int n, int m) {
if (m == 0 || n == m)
return 1;
return cmb(n - 1, m - 1) + cmb(n - 1, m);
}
2.直接算
组合数的公式是:
这个东西要证明也不难,我们知道排列数 \(P_n^m = \frac{n!}{(n-m)!}\),表示从 \(n\) 个数中选 \(m\) 个数的一个排列。这个就很好证了,因为第一个数有 \(n\) 种选择,第二个数有 \(n-1\) 种选择,以此类推。而组合数中每一次选出的 \(m\) 个数是不考虑顺序的,由于 \(m\) 个数的排列有 \(m!\) 种,所以 \(C_n^m = \frac{P_n^m}{m!}=\frac{n!}{m!(n-m)!}\) 。
上面那个式子其实可以写成这样:
因为每连续的 \(m\) 个自然数中,至少有一个 \(m\) 的倍数,所以我们可以从 \(n\) 开始乘,乘上第 \(i\) 个数时就顺带去除以 \(i\),这样复杂度就是 \(O(m)\) 的了。
code
long long cmb(int n, int m) {
long long ans = 1;
for (int i = n, j = 1; j <= m; i--, j++)
ans = ans * i / j;
return ans;
}
3.求逆元
对于一些题目,由于 \(n,m\) 的范围很大,导致 \(C_n^m\) 会很大,而高精度又太麻烦,所以就需要组合数对一个大质数取余。
上面两种方法杨辉三角复杂度太高,直接算无法在中间取余,于是我们需要一种新的方法。
回头看组合数的式子,如果我们可以预处理出阶乘,那么我们就可以 \(O(1)\) 的算出每个组合数,但是注意这里有一个除法,所以我们是不能对结成去取余的,不然会发生不能整除而且结果也会有很大不同。
在数论中,我们学过一个东西叫乘法逆元,就可以对一个分数取余,而我们又知道如何 \(O(n)\) 的推出 \(1\) 到 \(n\) 的阶乘的逆元,于是这个问题就变得很见简单了了,我们预处理出所有阶乘以及他们的逆元,根据逆元的定义可知:
于是我们就可以 \(O(n)\) 的算出来了。
4.卢卡斯定理
见这篇博客:卢卡斯定理学习笔记。