老题新解|组合数问题
《信息学奥赛一本通》第163题:组合数问题
题目描述
给定两个正整数 nnn 和 mmm,请你计算从 nnn 个不同的元素中选择 mmm 个元素的方案数(选择的顺序不重要)。
由于方案数可能很大,请输出方案数对 109+710^9+7109+7 取模的结果,也就是输出方案数除以 109+710^9+7109+7 的余数。
输入格式
一行,包含两个整数 nnn 和 mmm。
输出格式
一个整数,表示组合数 CnmC_n^mCnm 对 109+710^9+7109+7 取模的结果。
输入输出样例 #1
输入 #1
5 3
输出 #1
10
说明/提示
样例解释 #1:
从 555 个元素中选择 333 个,总共有 101010 种不同的方案:
- (1,2,3)(1,2,3)(1,2,3)
- (1,2,4)(1,2,4)(1,2,4)
- (1,2,5)(1,2,5)(1,2,5)
- (1,3,4)(1,3,4)(1,3,4)
- (1,3,5)(1,3,5)(1,3,5)
- (1,4,5)(1,4,5)(1,4,5)
- (2,3,4)(2,3,4)(2,3,4)
- (2,3,5)(2,3,5)(2,3,5)
- (2,4,5)(2,4,5)(2,4,5)
- (3,4,5)(3,4,5)(3,4,5)
注意:选择 (1,2,3) 和选择 (2,1,3) 被视为同一种方案。
数据范围:
对于 20% 的数据,满足 1≤m≤n≤101\le m\le n\le 101≤m≤n≤10
对于 100% 的数据,满足 1≤m≤n≤50001\le m\le n\le 50001≤m≤n≤5000
大家好,我是莫小特。
这篇文章给大家带来《信息学奥赛一本通》中的第 163 题:组合数问题。
一、题目描述
洛谷的题号是:B2164 组合数问题
二、题意分析
这道题是信息学奥赛一本通练习题的第 163 题。
根据输入格式,输入一行两个整数 m 和 n,数据范围:1≤m≤n≤50001\le m\le n\le 50001≤m≤n≤5000,使用 int 类型即可,注意输入顺序哦,n 在前,m 在后。
int m,n;
cin>>n>>m;
输入完成后,我们来分析题目,题目要求我们求从 n 个不同的元素中选择 m 个元素的方案数,注意,这里只需要求方案数量,具体的方案不需要我们输出,所以我们可以直接利用公式:
Cnm=n!m!(n−m)!C_n^m=\frac {n!}{m!(n−m)!} Cnm=m!(n−m)!n!
根据题目计算出结果之后,需要计算对 109+710^9+7109+7 求模的结果。
先定义这个数值出来作为常量。
const long long MOD = 1000000007;
此时可以使用阶乘+逆阶乘的方法来计算。
模 p(这里 p=109+7p=10^9+7p=109+7,是素数)下,除法用乘以模逆元替代:
a−1≡ap−2(modp)a^{-1} \equiv a^{p-2} \pmod p a−1≡ap−2(modp)
所以得出:
Cnm≡n!×(m!)−1×((n−m)!)−1(modp))C_n^m≡n!×(m!)_{−1}×((n−m)!)^{−1}\pmod p) Cnm≡n!×(m!)−1×((n−m)!)−1(modp))
所以需要定义一个数组,元素个数为 5005。
const int MAXN=5005;
定义一个 long long 类型数组。
long long fac[MAXN],ifac[MAXN];
计算快速幂,二分幂,将指数 b 按二进制分解,遇到 1 就把当前基 a 乘到结果上。
注意:在这段代码中没有 a %= MOD
的初始语句。这里没有问题,因为我们只把 fac[n]
传给 modpow
(它已经被 % MOD
),但一般化写法建议先 a %= MOD
。
long long modpow(long long a, long long b) {long long res = 1;while (b) {if (b & 1) res = res * a % MOD; // 当 b 的当前最低位为 1 时,累乘一个 aa = a * a % MOD; // a 翻倍(平方)b >>= 1; // b 右移一位(相当于除以 2)}return res;
}
从 1 到 n 逐步累乘并取模,得到 0!
到 n!
的模值。
这样做避免了重复计算阶乘,若题目中需要多次查询组合数非常高效。
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
第一步:用费马小定理计算 ifac[n] = (n!)^{-1}
(只需一次快速幂)。
第二步:利用关系 (i-1)!^{-1} = i!^{-1} * i
推出 ifac[i-1] = ifac[i] * i % MOD
,从 n
反推到 0
,不需要对每个 i
单独做幂运算,效率高且代码简洁。
- 思考:这样我们得到完整的
(i!)^{-1}
表,方便直接索引ifac[m]
与ifac[n-m]
。
ifac[n] = modpow(fac[n], MOD - 2); // (n!)^{-1}
for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;
最后输出,直接套公式 n! * (m!)^{-1} * ((n-m)!)^{-1}
,每步取模以避免溢出。
long long ans = fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;
cout << ans << '\n';
按照样例输入对数据进行验证。
符合样例输出,到网站提交测评。
测试通过!
三、完整代码
该题的完整代码如下:
#include <bits/stdc++.h>
using namespace std;const long long MOD = 1000000007;
const int MAXN = 5005;// 快速幂:求 a^b % MOD
long long modpow(long long a, long long b) {long long res = 1;while (b) {if (b & 1) res = res * a % MOD;a = a * a % MOD;b >>= 1;}return res;
}int main() {int n, m;cin >> n >> m; // 输入顺序:n 在前,m 在后static long long fac[MAXN], ifac[MAXN];fac[0] = 1;for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;ifac[n] = modpow(fac[n], MOD - 2); // 费马小定理求逆元for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % MOD;long long ans = fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;cout << ans << '\n';return 0;
}
四、总结
常见易错点:
1、忘记处理 m=0
或 m=n
的情况
虽然通用公式也能处理,但在实现中容易越界(例如索引 ifac[n-m]
),确认 0! = 1
已包含在 fac
/ifac
数组里即可。结果应为 1
。
2、模数性质误用(非素数模)
本题模 MOD = 10^9+7
是素数,可以用费马小定理计算逆元 a^{MOD-2}
。如果换成非素数模,不能用费马小定理,需要用扩展欧几里得或其他方法。
3、快速幂初始值或取模位置错误导致溢出
modpow
内部应先对 a %= MOD
(若上层可能传入未取模的值),循环中每次乘法都要 % MOD
。否则中间结果可能溢出 long long
(尽管 C++ 64 位通常够用,但保持规范更安全)。
4、数组大小不足或越界
n
最大 5000,数组定义要至少 MAXN = 5005
(或 n+1
)。fac[0..n]
、ifac[0..n]
都会被访问。不要定义成太小。
5、把除法当作普通除法而非模逆
直接写 fac[n] / (fac[m]*fac[n-m]) % MOD
是错误的。除法在模意义下需要乘以逆元。
6、类型不够 / 溢出
中间乘法 fac[i-1]*i
应在 long long
下进行并取模。使用 int
会溢出。
7、重复计算逆元导致超时(低效做法)
不要对每个 i
都调用二分幂求逆元(O(n log MOD)
次方),可以先算 ifac[n]
,再递推 ifac[i-1] = ifac[i]*i%MOD
(仅一次快速幂,整体 O(n)
),这是常用优化。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注我哦!
如果有更好的方法也可以在评论区评论哦,我都会看哒~
我们下集见~