利用 Lucas 定理将大组合数按质数模数 p 分解为 p 进制位,结合快速幂与逆元计算 C(n, m) mod p。
OJ: roj
题目 ID: 1650
难度:提高
标签:数论组合数学Lucas定理
创建: 2026-09-30 23:55
更新: 2026-10-07 13:50
形式化题目
给定
正解
思路
当
Lucas 定理指出,对于任意非负整数
从进制的角度来看,相当于将
对于每一位的小组合数
- 若
,则方案数为 ,整体答案直接为 ; - 若
,因为题设保证 ,所以 的规模很小。 我们可以直接将分子 与分母 分别连乘取模。由于 且 为质数,分母中所有数与 互质,利用费马小定理通过快速幂计算分母模 的逆元 即可完成单项计算。
整体使用迭代循环提取
代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-03-31 10:00
# update_at: 2026-03-31 10:00
import sys
from functools import reduce
def comb_small(n: int, m: int, p: int) -> int:
"""计算单步较小规模组合数 C(n, m) mod p,要求 n, m < p。"""
if m < 0 or m > n:
return 0
m = min(m, n - m)
# 分子 (n-m+1)*...*n,分母 1*...*m
num = reduce(lambda a, b: a * b % p, range(n - m + 1, n + 1), 1)
den = reduce(lambda a, b: a * b % p, range(1, m + 1), 1)
return num * pow(den, p - 2, p) % p
def lucas(n: int, m: int, p: int) -> int:
"""根据 Lucas 定理计算 C(n, m) mod p,将 n, m 转为 p 进制逐位计算。"""
ans = 1
while n > 0 or m > 0:
ans = ans * comb_small(n % p, m % p, p) % p
n //= p
m //= p
return ans
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
T = next(data)
out = [str(lucas(next(data), next(data), next(data))) for _ in range(T)]
print("\n".join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 00:35
* update_at: 2026-10-06 00:35
*/
#include <cstdio>
using namespace std;
typedef long long ll;
// 快速幂:计算 base^exp mod mod,用于费马小定理求逆元
ll fast_pow(ll base, ll exp, ll mod) {
ll res = 1;
base %= mod;
while (exp > 0) {
if (exp & 1) res = res * base % mod;
base = base * base % mod;
exp >>= 1;
}
return res;
}
// 计算单步组合数 C(n, m) mod p,要求 n, m < p 且 p 为质数
ll comb_small(ll n, ll m, ll p) {
if (m < 0 || m > n) return 0;
if (m > n - m) m = n - m; // 取较小的一侧,减少连乘项数
ll num = 1; // 分子 (n-m+1) * ... * n
ll den = 1; // 分母 1 * ... * m
for (ll i = 1; i <= m; i++) {
num = num * ((n - m + i) % p) % p;
den = den * (i % p) % p;
}
// 分母与 p 互质(p 为质数且 p > m),用费马小定理求逆元
return num * fast_pow(den, p - 2, p) % p;
}
// Lucas 定理:把 n, m 按 p 进制逐位拆开,各位小组合数相乘
ll lucas(ll n, ll m, ll p) {
ll ans = 1;
while (n > 0 || m > 0) {
ans = ans * comb_small(n % p, m % p, p) % p;
if (ans == 0) return 0; // 某一位出现 C(a_i, b_i) = 0,整体即为 0
n /= p;
m /= p;
}
return ans;
}
int main() {
ll T;
scanf("%lld", &T);
while (T--) {
ll n, m, p;
scanf("%lld %lld %lld", &n, &m, &p);
printf("%lld\n", lucas(n, m, p));
}
return 0;
}复杂度
- 时间复杂度:
将
转换为 进制位需要 步。每一位计算小组合数时,连乘项数不超过 项,求逆元耗时 。 因此单组数据时间复杂度为 。由于 且 ,在极限情况下运算量也非常小,总时间复杂度为 。 - 空间复杂度:
仅需存储常数个临时变量,辅助空间复杂度为
。
总结
当组合数