组合数学习笔记

定义

组合数通常写作 \(C_n^m\), 表示从 \(n\) 个数中选 \(m\) 个数的方法数。

怎么算

1.杨辉三角

这个东西是一个递推式,如下:

\[C_n^m = C_{n-1}^{m-1}+C_{n-1}^m \]

这样我们可以去递推或者递归去求一个组合数,时间复杂度是 \(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.直接算

组合数的公式是:

\[C_n^m = \frac{n!}{m!(n-m)!} \]

这个东西要证明也不难,我们知道排列数 \(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)!}\)

上面那个式子其实可以写成这样:

\[C_n^m = \frac{n(n-1)(n-2)...(n-m+1)}{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\) 的阶乘的逆元,于是这个问题就变得很见简单了了,我们预处理出所有阶乘以及他们的逆元,根据逆元的定义可知:

\[C_n^m \equiv \frac{n!}{m!(n-m)!}\equiv n! \times m!^{-1} \times (n-m)!^{-1} \pmod p \]

于是我们就可以 \(O(n)\) 的算出来了。

4.卢卡斯定理

见这篇博客:卢卡斯定理学习笔记

一些习题

posted @ 2022-12-04 10:20  rlc202204  阅读(23)  评论(0编辑  收藏  举报