组合数常见的四种计算方法
杨辉三角法
这是最直观、最易于理解的方法之一。它基于组合数的递推关系:
C(n, k) = C(n-1, k-1) + C(n-1, k)
这个公式正是杨辉三角的生成规则。
算法步骤:
创建一个二维数组
dp
,大小为(n+1) x (n+1)
。初始化边界条件:对于所有的
i
,C(i, 0) = 1
且C(i, i) = 1
。使用双重循环来填充这个表格:
外层循环
i
从 0 到 n,代表总元素数。内层循环
j
从 1 到i-1
(因为j=0
和j=i
的情况已经初始化),应用递推公式dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
。
最终,
dp[n][k]
就是我们要的结果。
#include <stdio.h>#define MAX_K 1000 // 根据需求调整最大k值// 杨辉三角法计算组合数(固定数组)
long long comb_dp(int n, int k) {if (k < 0 || k > n) return 0;if (k == 0 || k == n) return 1;// 利用对称性优化if (k > n - k) k = n - k;long long dp[MAX_K + 1] = {0};dp[0] = 1;for (int i = 1; i <= n; i++) {// 反向遍历避免覆盖for (int j = (i < k ? i : k); j > 0; j--) {dp[j] = dp[j] + dp[j - 1];}}return dp[k];
}
乘法公式法
直接使用组合数的定义公式,但在计算时进行优化以避免中间值过大和精度丢失。
公式: C(n, k) = n! / (k! * (n-k)!)
直接计算阶乘再相除,很容易导致数值溢出。一个更好的方法是边乘边除。
算法步骤:
确保 k <= n-k,如果不满足,令 k = n - k。因为 C(n, k) = C(n, n-k),这样可以减少计算量。
计算结果 = 1。
通过一个循环从 1 到 k,每次迭代执行:
结果 = 结果 * (n - i + 1) / i
这个方法的正确性在于:
C(n, k) = [n * (n-1) * ... * (n-k+1)] / [1 * 2 * ... * k]
在每一步乘法后立即进行除法,可以保证中间结果尽可能小,并且最终结果是一个整数。
#include <stdio.h>// 乘法公式计算组合数
long long comb_mult(int n, int k) {if (k < 0 || k > n) return 0;if (k == 0 || k == n) return 1;// 利用对称性优化if (k > n - k) k = n - k;long long result = 1;for (int i = 1; i <= k; i++) {result = result * (n - k + i) / i;}return result;
}
模逆元法
在编程竞赛和密码学中,我们经常需要计算 C(n, k) mod p,其中 p 可能是一个大质数(如 10^9+7)。
情况一:p 是质数(且足够大,通常大于 n)
这是最理想的情况,我们可以利用费马小定理和模逆元。
原理:
费马小定理:如果 p 是质数,且 a 不是 p 的倍数,则 a^(p-1) ≡ 1 (mod p)。
由此可得,a 在模 p 下的逆元 a^(-1) ≡ a^(p-2) (mod p)。
那么,组合数公式变为:
C(n, k) mod p = [ n! * inv(k!) * inv((n-k)! ) ] mod p
其中 inv(x)
表示 x 在模 p 下的逆元。
算法步骤:
预处理出 1 到 n 的阶乘
fact[i]
和对应的阶乘的逆元inv_fact[i]
。计算
C(n, k) = fact[n] * inv_fact[k] % p * inv_fact[n-k] % p
。
#include <stdio.h>
#include <stdlib.h>#define MOD 1000000007
#define MAX_N 1000000long long fact[MAX_N + 1];
long long inv_fact[MAX_N + 1];// 快速幂
long long power(long long a, long long b) {long long res = 1;while (b > 0) {if (b & 1) res = (res * a) % MOD;a = (a * a) % MOD;b >>= 1;}return res;
}// 预处理阶乘和逆元
void precompute() {fact[0] = 1;for (int i = 1; i <= MAX_N; i++) {fact[i] = (fact[i - 1] * i) % MOD;}inv_fact[MAX_N] = power(fact[MAX_N], MOD - 2);for (int i = MAX_N - 1; i >= 0; i--) {inv_fact[i] = (inv_fact[i + 1] * (i + 1)) % MOD;}
}// 模逆元法计算组合数
long long comb_mod(int n, int k) {if (k < 0 || k > n) return 0;return (fact[n] * inv_fact[k] % MOD) * inv_fact[n - k] % MOD;
}
卢卡斯定理
情况二:p 可能不是质数,或者 n, k 极大(如 10^18)
这时需要使用 Lucas 定理。
卢卡斯定理: C(n,k) mod p = C(n mod p, k mod p) × C(n/p, k/p) mod p
将n和k表示为p进制数,组合数等于各位组合数的乘积。
这通常用于 p 不太大的情况。
算法步骤
如果k=0,返回1
递归计算:Lucas(n,k,p) = C(n%p, k%p) × Lucas(n/p, k/p, p) mod p
其中C(n%p, k%p)用简单方法计算(因为n%p, k%p < p)
#include <stdio.h>#define MOD 1000000007// 计算小组合数(n, k < MOD)
long long comb_small(int n, int k, int p) {if (k < 0 || k > n) return 0;if (k == 0 || k == n) return 1;if (k > n - k) k = n - k;long long res = 1;for (int i = 1; i <= k; i++) {res = res * (n - k + i) % p;res = res * power(i, p - 2, p) % p;}return res;
}// 带模数的快速幂
long long power(long long a, long long b, int p) {long long res = 1;while (b > 0) {if (b & 1) res = (res * a) % p;a = (a * a) % p;b >>= 1;}return res;
}// 卢卡斯定理计算组合数
long long lucas(int n, int k, int p) {if (k == 0) return 1;return lucas(n / p, k / p, p) * comb_small(n % p, k % p, p) % p;
}
总结
方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
杨辉三角(DP) | O(n*k) | O(k) (优化后) | 需要多次计算不同的小规模组合数,适合预处理。 |
乘法公式 | O(k) | O(1) | 计算单个组合数的最快方法,n 和 k 不大,且结果在整数范围内。最常用。 |
模逆元(p为质数) | O(n) 预处理,O(1) 查询 | O(n) | n, k 很大,需要对结果取模,且模数 p 是质数。编程竞赛标配。 |
卢卡斯定理 | O(p * log_p n) | O(1) | n, k 极大,但模数 p 相对较小。 |