#zHSXybttg060603. 1650:【例 3】组合
1650:【例 3】组合
1650:【例 3】组合
题目描述
给出组合数 表示从 个元素中选出 个元素的方案数。例如 。可是当 比较大的时候, 很大。于是 xiaobo 希望你输出 的值。
输入格式
输入数据第一行是一个正整数 ,表示数据组数;
接下来是 组数据,每组数据有 个正整数 。
输出格式
对于每组数据,输出一个正整数,表示 的结果。
样例
样例输入 #1
2
5 2 3
5 2 61
样例输出 #1
1
10
样例解释 #1
- 第一组:,。
- 第二组:,。
数据范围
对于所有数据:
- 是素数
时空限制
- 时间限制:1000 ms
- 内存限制:524288 KB
注意: 很大,但 较小(),且 是素数。可以用组合数公式:
$$C(n,m) = \frac{n!}{m!(n-m)!} = \frac{n \cdot (n-1) \cdots (n-m+1)}{m!}$$因为 不大,可以直接计算分子乘积模 ,再乘以分母的逆元(模 )。由于 是素数,可以用费马小定理求逆元。
但需要注意:如果 很大,但 可能小于 ,此时 中可能包含 的因子,导致分母的逆元不存在(因为分母可能与 不互质)。但题目保证 ,且 是素数,所以 中的因子都不包含 (因为 ),所以 与 互质,其逆元存在。然而分子 中可能包含 的倍数,此时乘积模 可能为 ,但实际组合数可能不被 整除?例如 ,模 为 。所以我们需要处理分子中 的因子。
由于 是素数,且 ,所以分母 不含 。分子是连续 个整数的乘积,如果这 个数中包含 的倍数,则组合数模 为 ?不一定,因为组合数可能是整数,可能约分后消去 。例如 ,模 为 。但 ,分子含 ,但分母 与 互质,所以组合数模 为 (因为 整除组合数)。所以当分子中包含 的倍数时,组合数模 可能为 。
更一般地,组合数 模素数 可以用 Lucas 定理 计算,但 Lucas 定理适用于 较小的情况(因为需要计算 模 的组合数)。这里 可以很大(),但 较小(),且 ,所以我们可以直接使用公式计算,但需要处理分子中 的因子。
因为 是素数,分子中最多有一个 的倍数(因为连续 个数中, 的倍数最多 个,但 ,所以最多 个)。设分子中第 项 是 的倍数,则该项模 为 ,整个乘积模 为 。但组合数可能约分后不为 ?例如 ,模 为 。,模 为 。所以如果分子中包含 的倍数,则组合数模 为 。因为分母 不含 ,无法约掉 。所以结论:如果 中存在 的倍数,则 。
否则,分子与 互质,我们可以直接计算分子模 ,再乘以分母的逆元。
因此,算法步骤:
- 读入 。
- 对于每组数据,读入 。
- 检查 中是否存在 的倍数。由于 ,可以枚举这 个数,但 可能很大,我们不能直接生成这些数。我们可以计算 ,然后看区间 是否包含 的倍数。即是否存在整数 使得 。这等价于 。如果成立,则存在 的倍数,输出 。
- 否则,计算分子 。因为 ,可以直接循环 次,每次乘 并取模 。
- 计算分母 ,同样循环计算。
- 计算 的逆元 (因为 是素数,可以用费马小定理:)。
- 输出 。
注意: 可能小于 ,但题目保证 ,所以 。
由于 可能很大(),快速幂求逆元需要 ,总复杂度 ,可以接受。
但检查是否存在 的倍数时,需要注意整数溢出。可以用以下方法:
- 设 ,。
- 如果 ,则区间包含负数,但 是正数,所以 的倍数可能是负数?但组合数定义中 为正整数, 都是正整数(因为 ),所以 。实际上 ,所以 。
- 计算 ,如果 且 ,则存在 的倍数。
由于 可能很大,直接用除法判断:
if (n / p != (n-m) / p) // 因为 (n-m) 即 n-m+1-1,注意区间是 [n-m+1, n]
更精确地:设 , 。如果 ,则存在 的倍数。即 if (b/p > (a-1)/p)。
但注意整数除法下取整。
因此,检查代码:
long long a = n - m + 1;
if (n / p != (a-1) / p) {
// 存在 p 的倍数
cout << 0 << endl;
}
注意: 都是 long long 范围。
如果不存在 的倍数,则计算分子分母。
但 最大 ,循环 次乘法取模是可行的。
注意: 是素数,但 可能很小(大于 但可能接近 ),但 ,所以 至少为 。
因此,完整算法:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll pow_mod(ll a, ll b, ll p) {
ll res = 1;
while (b) {
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
int main() {
int T;
cin >> T;
while (T--) {
ll n, m, p;
cin >> n >> m >> p;
ll a = n - m + 1;
// 检查区间 [a, n] 中是否存在 p 的倍数
if (n / p != (a-1) / p) {
cout << 0 << endl;
continue;
}
// 计算分子
ll num = 1;
for (ll i = 0; i < m; i++) {
num = num * (n - i) % p;
}
// 计算分母 m!
ll den = 1;
for (ll i = 1; i <= m; i++) {
den = den * i % p;
}
// 求逆元
ll inv = pow_mod(den, p-2, p);
ll ans = num * inv % p;
cout << ans << endl;
}
return 0;
}
但需要注意:当 很大时, 可能很大,快速幂需要 ,可以接受。
此外,如果 很小,但 非常大,循环 次乘法没问题。
注意:上述检查存在 的倍数的方法是否总是正确?考虑 ,区间 包含 ,,不等,正确。考虑 ,区间 包含 ,,不等。考虑 ,区间 不含 的倍数,,相等。所以条件正确。
特殊情况:当 ?题目 ,不考虑。
注意:当 很大时, 和 可能相等,即使区间包含 的倍数?例如 ,区间长度 ,如果 远大于 ,则区间内不可能有 的倍数,所以条件成立。所以检查是安全的。