组合数取模的应用背景
运行时间限制:1s;
运行空间限制:256M;
代码长度限制:2000000B
题目描述
高一八班有n个人,从1到n编号,一次互判作业时,老师随机将作业发到这n个人手中。已知有k个人拿到的不是自己的作业,那么请问有多少种情况符合条件呢?
输入
共1行,包含2个整数n和k。
输出
共1行,包含1个整数,表示答案。由于答案可能很大,请输出答案模10007的余数。
输入样例
4 3
输出样例
8
测试规模
对于30%的数据,0≤k≤n≤10。
另有10%的数据,k=0。
另有10%的数据,k=1。
对于70%的数据,0≤k≤n≤10000。
对于100%的数据,0≤k≤1000000,1≤n≤1000000000。
组合数 C n m C_n^{m} Cnm的计算
组合数取模 C n m % p C_n^{m}\%p Cnm%p的计算
方法一:通过递推公式计算
该算法可以支持 m ≤ n ≤ 1000 m≤n≤1000 m≤n≤1000的情况,对 p p p的大小和素性没有要求, p ≤ 1 0 9 p≤10^9 p≤109均可。
递归:
int res[1010][1010] = {0};
int C(int n, int m, int p){
if(m == 0 || m == n) return 1;
if(res[n][m] != 0) return res[n][m]; //C(n, m)已计算过
return res[n][m] = C(n - 1, m) + C(n - 1, m - 1) % p; //先计算,再赋值,并返回
}
递推:
//将底数从2~n的所有组合数的表计算出来
long long res[100][100] = {0};
void calC(int n){
for(int i = 0; i <= n; i++)
res[i][0] = res[i][i] = 1; //初始化边界,组合数C(n, 0) = C(n, n) = 1
for(int i = 2; i <= n; i++){
for(int j = 1; j <= i / 2; j++){
res[i][j] = res[i - 1][j] + res[i - 1][j - 1] % p; //递推计算C(i, j)
res[i][i - j] = res[i][j];
}
}
}
//直接输出组合数C(n, m)
long long C(long long n, m){
return res[n][m];
}
方法二:根据定义式计算
对组合数进行质因子分解,若有
C
n
m
=
p
1
c
1
×
p
2
c
2
×
⋯
×
p
k
c
k
C_n^{m}=p_1^{c_1}\times p_2^{c_2}\times\cdots\times p_k^{c_k}
Cnm=p1c1×p2c2×⋯×pkck,那么
C
n
m
%
p
=
p
1
c
1
×
p
2
c
2
×
⋯
×
p
k
c
k
%
p
C_n^{m}\%p=p_1^{c_1}\times p_2^{c_2}\times\cdots\times p_k^{c_k}\%p
Cnm%p=p1c1×p2c2×⋯×pkck%p,于是可以对每组
p
i
c
i
%
p
p_i^{c_i}\%p
pici%p,然后相乘取模得到结果。
如何对
C
n
m
C_n^{m}
Cnm进行质因子分解? 考虑到
C
n
m
=
n
!
m
!
(
n
−
m
)
!
C_n^{m}= \frac {n!} {m!(n-m)!}
Cnm=m!(n−m)!n!,只需遍历不超过n的所有质数
p
i
p_i
pi,然后分别计算
n
!
n!
n!、
m
!
m!
m!和
(
n
−
m
)
!
(n-m)!
(n−m)!中含有
p
i
p_i
pi的个数
x
x
x、
y
y
y、
z
z
z,即可计算出
C
n
m
C_n^{m}
Cnm中质因子
p
i
p_i
pi的个数为
x
−
y
−
z
x-y-z
x−y−z。
如何求
n
!
n!
n!中含质因子
p
i
p_i
pi的个数? 求
n
!
n!
n!中有多少个质因子
p
p
p
该方法的时间复杂度为:
O
(
k
l
o
g
n
)
O(klogn)
O(klogn),其中
k
k
k为不超过
n
n
n的质数个数。适用于
m
≤
n
≤
1
0
6
m\leq n\leq10^6
m≤n≤106的数据,对
p
p
p的大小和素性没有额外要求。
代码如下:
//使用筛法得到素数表prime,由于n可能为素数,故表中最大素数不得小于n
int prime[maxn];
//计算C(n, m)%p
int C(int n, int m, int p){
int ans = 1;
//遍历不超过n的所有质数
for(int i = 0; prime[i] <= n; i++){
//计算C(n, m)中prime[i]的指数c,cal(n, k)为n!中含质因子k的个数
int c = cal(n, prime[i]) - cal(m, prime[i]) - cal(n - m, prime[i]) //c = x - y - z
//快速幂计算prime[i]^c%p
ans = ans * binaryPow(prime[i], c, p) % p;
}
return ans;
}
注:快速幂运算。
方法三:Lucas定理
如果
p
p
p是素数,将
m
m
m和
n
n
n表示为
p
p
p进制:
m
=
m
k
p
k
+
m
k
−
1
p
k
−
1
+
⋯
+
m
0
n
=
n
k
p
k
+
n
k
−
1
p
k
−
1
+
⋯
+
n
0
m=m_kp^k+m_{k-1}p^{k-1}+\cdots+m_0\\ n=n_kp^k+n_{k-1}p^{k-1}+\cdots+n_0
m=mkpk+mk−1pk−1+⋯+m0n=nkpk+nk−1pk−1+⋯+n0那么Lucas定理有:
C
n
m
≡
C
n
k
m
k
×
C
n
k
−
1
m
k
−
1
×
⋯
×
C
n
0
m
0
(
m
o
d
p
)
C_n^{m}\equiv C_{n_k}^{m_k}\times C_{n_k-1}^{m_k-1}\times\cdots\times C_{n_0}^{m_0}(mod p)
Cnm≡Cnkmk×Cnk−1mk−1×⋯×Cn0m0(modp)成立。
时间复杂度为:
O
(
l
o
g
n
)
O(logn)
O(logn),适用于
m
≤
n
≤
1
0
18
m\leq n\leq10^{18}
m≤n≤1018的数据,且要求
p
≤
1
0
5
p\leq10^5
p≤105并同时为素数。
代码如下:
int Lucas(int n, int m){
if(m == 0) return 1;
return C(n % p, m % p) * Lucas(n / p, m / p) % p;
}
总结
方法 | n | m | p |
---|---|---|---|
方法一 | n ≤ 1 0 4 n\leq 10^4 n≤104 | m ≤ 1 0 4 m\leq 10^4 m≤104 | p ≤ 1 0 9 p\leq 10^9 p≤109 |
方法二 | n ≤ 1 0 6 n\leq 10^6 n≤106 | m ≤ 1 0 6 m\leq 10^6 m≤106 | p ≤ 1 0 9 p\leq 10^9 p≤109 |
Lucas | n ≤ 1 0 18 n\leq 10^{18} n≤1018 | m ≤ 1 0 18 m\leq 10^{18} m≤1018 | p ≤ 1 0 5 p\leq 10^5 p≤105且为素数 |
参考资料
[1]. 《算法笔记》P181-190
[2]. 组合数取模的题