数学问题——组合数
一、关于 n!的一个问题
n! 表示 n 的阶乘,我们讨论一下关于它的一个问题:求 n! 中有多少个质因子 p。
举个例子,6! = 1*2*3*4*5*6 ,于是 6! 中有 4 个质因子 2,两个质因子 3。
对这个问题,直观的想法是计算 1~n 的每个数各有多少个质因子 p,然后将结果累加,时间复杂度为 O(nlogn)。
我们需要寻求速度更快的方法。现在考虑 10! 中质因子 2 的个数,显然 10! 中有 21 的数的个数为 5,有因子 22 的数的个数为 2,有因子 23 的数的个数为 1,因此 10! 中质因子 2 的个数为 5+2+1=8。
仔细思考便可发现此过程可以推广为:n! 中有 $\left ( \frac{n}{p}+\frac{n}{p^{2}}+\frac{n}{p^{3}}+... \right )$ 个质因子 p。代码如下:
1 // 计算 n! 中有多少个质因子 p 2 int cal(int n, int p) { 3 int ans = 0; 4 while(n) { 5 ans += n/p; // 累加 n/p^k 6 n /= p; 7 } 8 return ans; 9 }
利用这个算法,可以很快计算出 n! 的末尾有多少个零:由于末尾 0 的个数等于 n! 中因子 10 的个数,而这又等于 n! 中质因子 5 的个数,因此只需要带入 cal(n,5) 就可以得到结果。
二、组合数的计算
组合数 $C_{n}^{m}$ 是指从 n 个不同元素中选出 m 个元素的方案数 (m≤n),其定义式为 $C_{n}^{m}=\frac{n!}{m!\left ( n-m \right )!}$ 。本节主要讨论如下两个问题:
- 如何计算 $C_{n}^{m}$
- 如何计算 $C_{n}^{m}\%p$
1. 如何计算 $C_{n}^{m}$
方法一:通过定义式直接计算
方法二:通过递推公式计算
由于 $C_{n}^{m}$ 表示从 n 个不同的数中选 m 个数的方案数,因此可以转换为下面两种选法的方案数之和:一是不选最后一个数,从前 n-1 个数中选 m 个数;二是选最后一个数,从前 n-1 个数中选 m-1 个数。于是就可以得到下面这个递推式:
$C_{n}^{m}=C_{n}^{m-1}+C_{n-1}^{m-1}$
而由定义可知 $C_{n}^{0}=C_{n}^{n}=1$,这正好可以作为递归的边界,代码如下:
1 // 计算组合数 Cnm,递归形式 2 long long C(long long n, long long m) { 3 if(m==0 || n==m) return 1; 4 return C(n-1,m) + C(n-1,m-1); 5 }
以上代码会造成一个问题:重复计算。因此不妨记录下已经计算过的 C(n,m),这样当下次碰到时就可以作为结果直接返回了,代码如下:
1 long long res[67][67] = {0}; // 记录结果 2 // 计算组合数 Cnm,递归形式 3 long long C(long long n, long long m) { 4 if(m==0 || n==m) return 1; 5 if(res[n][m] != 0) // 已经计算过 6 return res[n][m]; 7 return res[n][m] = C(n-1,m) + C(n-1,m-1); // 赋值并返回 8 }
方法三:通过定义式的变形来计算
定义式可进行如下化简:
$C_{n}^{m}=\frac{n!}{m!\left ( n-m \right )!}=\frac{(n-m-1)*(n-m+2)*...*(n-m+m)}{1*2*...*m}=\frac{\frac{\frac{(n-m+1)*(n-m+2)}{2}*...}{...}*(n-m+m)}{m}$
代码如下:
1 // 通过定义式的变形来计算组合数 Cnm 2 long long C1(long long n, long long m) { 3 long long ans = 1; 4 long long i; 5 for(i=1; i<=m; ++i) { 6 ans = ans * (n-m+i) / i; 7 } 8 return ans; 9 }
完整测试代码如下:
1 /* 2 组合数的计算 3 */ 4 5 #include <stdio.h> 6 #include <string.h> 7 #include <math.h> 8 #include <stdlib.h> 9 #include <time.h> 10 #include <stdbool.h> 11 12 long long res[67][67] = {0}; // 记录结果 13 // 计算组合数 Cnm,递归形式 14 long long C(long long n, long long m) { 15 if(m==0 || n==m) return 1; 16 if(res[n][m] != 0) // 已经计算过 17 return res[n][m]; 18 return res[n][m] = C(n-1,m) + C(n-1,m-1); // 赋值并返回 19 } 20 21 // 通过定义式的变形来计算组合数 Cnm 22 long long C1(long long n, long long m) { 23 long long ans = 1; 24 long long i; 25 for(i=1; i<=m; ++i) { 26 ans = ans * (n-m+i) / i; 27 } 28 return ans; 29 } 30 31 int main() { 32 long long n, m; 33 while(scanf("%lld%lld", &n, &m) != EOF) { 34 printf("%lld\n", C(n, m)); 35 printf("%lld\n", C1(n, m)); 36 } 37 38 return 0; 39 }
2. 如何计算 $C_{n}^{m}\%p$
方法一、通过递推公式计算
这种方法基于第一个问题的方法二,只需要在原先的代码中适当的地方对 p 取模即可。代码如下:
1 int resp[1010][1010] = {0}; 2 // 求组合数 Cnm 对 p 取模,递归 3 int Cp(int n, int m, int p) { 4 if(m==0 || n==m) return 1; 5 if(res[n][m] != 0) // 已经计算过 6 return resp[n][m]; 7 return resp[n][m] = (C(n-1,m) + C(n-1,m-1))%p; // 赋值并返回 8 }