组合数
定义
\qquad 从 n n n 个不同元素中取出 m ( m ≤ n ) m(m\le n) m(m≤n) 个元素的所有组合的个数,叫做从 n n n 个不同元素中取出 m m m 个元素的组合数,记作 C n m C_n^m Cnm 或者 ( n m ) \left(n \atop m\right) (mn)。
公式
C n m = n ( n − 1 ) ( n − 2 ) ⋯ ( n − m + 1 ) 1 × 2 × 3 × ⋯ × m = n ! m ! ( n − m ) ! C_n^m=\frac{n(n-1)(n-2)\cdots(n-m+1)}{1\times2\times3\times\cdots\times m}=\frac{n!}{m!(n-m)!} Cnm=1×2×3×⋯×mn(n−1)(n−2)⋯(n−m+1)=m!(n−m)!n!
\qquad 特别地, C n 0 = 1 C_n^0=1 Cn0=1.
性质
- 互补: C n m = C n n − m C_n^m=C_n^{n-m} Cnm=Cnn−m.
- 组合恒等式: C n m = C n − 1 m + C n − 1 m − 1 C_n^m=C_{n-1}^m+C_{n-1}^{m-1} Cnm=Cn−1m+Cn−1m−1.
应用
\qquad 针对不同的数据范围应采用不同的算法。
一、885. 求组合数 I - AcWing题库
题目
\qquad 给定 n n n 组询问,每组询问给定两个整数 a , b a,b a,b,请你输出 C a b m o d ( 1 0 9 + 7 ) C^b_a mod\;(10^9+7) Cabmod(109+7) 的值。
输入格式
\qquad 第一行包含整数 n n n。
\qquad 接下来 n n n 行,每行包含一组 a a a 和 b b b。
输出格式
\qquad 共 n n n 行,每行输出一个询问的解。
数据范围
\qquad 1 ≤ n ≤ 1 × 1 0 4 , 1 ≤ b ≤ a ≤ 2 × 1 0 3 1 \le n\le 1\times10^4,\;1\le b\le a\le2\times10^3 1≤n≤1×104,1≤b≤a≤2×103
分析
\qquad 此题数据量较大,如果对每组数据分别进行计算的话,要算 2 × 1 0 7 2\times10^7 2×107 组数据,很可能会超时。但 a a a 和 b b b 都较小,如果先预处理出所有可能的 C a b C_a^b Cab 的话,只需要计算 ( 2 × 1 0 3 ) 2 / 2 = 2 × 1 0 6 (2\times10^3)^2/2=2\times10^6 (2×103)2/2=2×106 组数据,也就是说 n n n 组询问里有很多是重复询问的,无需每次都计算一遍。
时间复杂度
\qquad O ( n 2 ) O(n^2) O(n2)
代码
#include <iostream>
using namespace std;
const int N = 2e3 + 5;
const int mod = 1e9 + 7;
int c[N][N];
void init() { //预处理出所有可能询问的数据
for (int i = 0; i < N; i++)
for (int j = 0; j <= i; j++) {
if (j == 0 || j == i)
c[i][j] = 1;
else //利用组合数恒等式递推计算
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
}
int main() {
init();
int n;
scanf("%d", &n);
while (n--) {
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", c[a][b]);
}
return 0;
}
二、886. 求组合数 II - AcWing题库
题目
\qquad 给定 n n n 组询问,每组询问给定两个整数 a , b a,b a,b,请你输出 C a b m o d ( 1 0 9 + 7 ) C^b_a mod\;(10^9+7) Cabmod(109+7) 的值。
输入格式
\qquad 第一行包含整数 n n n。
\qquad 接下来 n n n 行,每行包含一组 a a a 和 b b b。
输出格式
\qquad 共 n n n 行,每行输出一个询问的解。
数据范围
\qquad 1 ≤ n ≤ 1 × 1 0 4 , 1 ≤ b ≤ a ≤ 1 × 1 0 5 1 \le n\le 1\times10^4,\;1\le b\le a\le1\times10^5 1≤n≤1×104,1≤b≤a≤1×105
分析
\qquad 此题与上一题唯一的区别就是 a a a 和 b b b 可能的值都变大了,那么我们就不能再使用上一题的思路来求解了。
\qquad 由于 a b ( m o d p ) ≠ a ( m o d p ) b ( m o d p ) \frac{a}{b}\pmod p\ne \frac{a\pmod p}{b\pmod p} ba(modp)=b(modp)a(modp),因此我们在用公式求这个 C a b ( m o d p ) C_a^b\pmod p Cab(modp) 的值的时候就势必要求 i ! i ! i! 的逆元。
\qquad
因此这题我们用公式求解,先预处理出可能用到的
i
!
i!
i! 以及
i
!
i !
i! 的逆元,分别用 fac[i]
和 infac[i]
数组存储。
\qquad
最终
C
a
b
(
m
o
d
p
)
C_a^b\pmod p
Cab(modp) 就可以表示为:
(
f
a
c
[
a
]
∗
i
n
f
a
c
[
a
−
b
]
∗
i
n
f
a
c
[
b
]
)
m
o
d
p
\left(fac[a]*infac[a-b]*infac[b]\right)\;mod\;p
(fac[a]∗infac[a−b]∗infac[b])modp
\qquad
还有一点很关键:
\qquad
由于此题的
m
o
d
=
1
0
9
+
7
mod=10^9+7
mod=109+7,其恰好是一个质数,因此我们利用费马小定理直接用快速幂求逆元即可,即求 infac[i]=fastpow(fac[i],mod-2,mod}
.
\qquad
另:这里还可以换种求法,由于
(
n
!
)
−
1
=
(
(
n
−
1
)
!
×
n
)
−
1
=
(
n
−
1
)
!
−
1
×
n
−
1
(n!)^{-1}=((n-1)!\times n)^{-1}=(n-1)!^{-1}\times n^{-1}
(n!)−1=((n−1)!×n)−1=(n−1)!−1×n−1,所以也可以用infac[i]=infac[i-1]*fastpow(i,mod-2,mod)
这样的方法求解。
时间复杂度
\qquad 预处理 n n n 个数,每个数都要进行快速幂: l o g m o d log\; mod logmod.
\qquad O ( n l o g n ) O(nlogn) O(nlogn)
代码
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5;
const int mod = 1e9 + 7;
int fac[N], infac[N];
int fastpow(int base) { //快速幂
int res = 1;
int power = mod - 2;
while (power) {
if (power & 1)
res = (LL)res * base % mod; //LL不能丢
base = (LL)base * base % mod;
power >>= 1;
}
return res;
}
void init() { //预处理出所有可能询问的数据
fac[0] = infac[0] = 1;
for (int i = 1; i < N; i++) {
fac[i] = (LL)fac[i - 1] * i % mod; //LL不能丢
infac[i] = fastpow(fac[i]);
}
}
int main() {
init();
int n;
scanf("%d", &n);
while (n--) {
int a, b;
scanf("%d%d", &a, &b); //LL不能丢,要及时取模(mod了两次)
printf("%lld\n", (LL)fac[a] * infac[a - b] % mod * infac[b] % mod);
}
return 0;
}
三、887. 求组合数 III - AcWing题库
题目
\qquad 给定 n n n 组询问,每组询问给定两个整数 a , b , p a,b,p a,b,p,其中 p p p 是质数。请你输出 C a b ( m o d p ) C^b_a\pmod{p} Cab(modp) 的值。
输入格式
\qquad 第一行包含整数 n n n。
\qquad 接下来 n n n 行,每行包含一组 a , b , p a,b,p a,b,p。
输出格式
\qquad 共 n n n 行,每行输出一个询问的解。
数据范围
\qquad 1 ≤ n ≤ 20 , 1 ≤ b ≤ a ≤ 1 × 1 0 18 , 1 ≤ p ≤ 1 0 5 1 \le n\le 20,\;1\le b\le a\le1\times10^{18},\;1\le p\le10^5 1≤n≤20,1≤b≤a≤1×1018,1≤p≤105
分析
\qquad 此题的询问量很小,但是每次都可能询问很大的数字。考虑使用卢卡斯(lucas)定理:
令 n = s p + q , m = t p + r . ( 0 ≤ q , r ≤ p − 1 ) n=sp+q,m=tp+r.(0\le q,r\le p-1) n=sp+q,m=tp+r.(0≤q,r≤p−1),
则
C
n
m
=
C
s
p
+
q
t
p
+
r
≡
C
s
t
⋅
C
q
r
(
m
o
d
p
)
C_n^m=C_{sp+q}^{tp+r}\equiv C_s^t\cdot C_q^r\pmod{p}
Cnm=Csp+qtp+r≡Cst⋅Cqr(modp)
即
C
n
m
≡
C
n
%
p
m
%
p
⋅
C
n
/
p
m
/
p
(
m
o
d
p
)
C_n^m\equiv C_{n\%p}^{m\%p}\cdot C_{n/p}^{m/p}\pmod{p}
Cnm≡Cn%pm%p⋅Cn/pm/p(modp)
\qquad
这样处理之后,我们要求的组合数就在
1
0
5
10^5
105 以内了,可以选择直接用定义求解。但是由于此题 p 不唯一,因此不能沿用第二题的方法预处理出能用到的阶乘和逆元。
代码
#include <iostream>
using namespace std;
typedef long long LL;
int p;
int fastpow(int base) { //快速幂
int res = 1;
int power = p - 2;
while (power) {
if (power & 1)
res = (LL)res * base % p;
base = (LL)base * base % p;
power >>= 1;
}
return res;
}
int C(int a, int b) { //用定义求解组合数
int res = 1;
for (int i = 1, j = a; i <= b; i++, j--) {
res = (LL)res * j % p;
res = (LL)res * fastpow(i) % p;
}
return res;
}
int lucas(LL a, LL b) { //卢卡斯定理的使用
if (a < p && b < p) return C(a, b);
return (LL)C(a % p, b % p) * lucas(a / p, b / p) % p;
}
int main() {
int n;
scanf("%d", &n);
while (n--) {
LL a, b;
scanf("%lld%lld%d", &a, &b, &p);
printf("%d\n", lucas(a, b));
}
return 0;
}
四、888. 求组合数 IV - AcWing题库
题目
\qquad 输入 a , b a,b a,b,求 C a b C_a^b Cab 的值。
\qquad 注意结果可能很大,需要使用高精度计算。
输入格式
\qquad 共一行,包含两个整数 a a a 和 b b b。
输出格式
\qquad 共一行,输出 C a b C_a^b Cab 的值。
数据范围
\qquad 1 ≤ b ≤ a ≤ 5000 1\le b\le a\le 5000 1≤b≤a≤5000
分析
\qquad 此题与前面截然不同,此题不再需要对答案取模,而是求出精确值。如果按照公式计算,那么需要写一个高精度乘法和一个高精度除法,这是很复杂的。
\qquad 在这种情况下,由于组合数的值一定是一个整数,我们可以对分子分母分别进行质因数分解,然后进行约分,这样就只需要进行高精度乘法运算,而无需再写复杂的高精度除法运算了。
对 n! 进行质因数分解:
\qquad
以
10
!
10!
10! 为例,我们想知道将这个数质因数分解后
2
2
2 的指数为几(res)。首先我们知道
10
!
=
1
×
2
×
3
×
⋯
×
10
10!=1\times2\times3\times\cdots\times10
10!=1×2×3×⋯×10,其中
2
,
4
,
6
,
8
,
10
2,4,6,8,10
2,4,6,8,10 均为
2
2
2 的倍数,那么先把这
5
5
5 个质因子加上(res += 10/2
);然后我们发现
4
,
8
4,8
4,8 中分别有
2
,
3
2,3
2,3 个质因数
2
2
2,而我们只算了一次,通过 res += 10 / 4
就可以把
4
,
8
4,8
4,8 分别再加一次;再通过 res += 10 / 8
可以把
8
8
8 再加一次。至此,我们用三次运算统计出了
10
!
10!
10! 中
2
2
2 出现了
8
8
8 次。(上述所有除法均为向下取整)
\qquad
总结一下,对于
n
!
n!
n! 来说,它分解质因数后质因子
p
p
p 的指数为:
⌊
n
p
1
⌋
+
⌊
n
p
2
⌋
+
⌊
n
p
3
⌋
+
⋯
+
⌊
n
p
k
⌋
,
(
p
k
≤
n
)
\lfloor\frac{n}{p^1}\rfloor+\lfloor\frac{n}{p^2}\rfloor+\lfloor\frac{n}{p^3}\rfloor+\cdots+\lfloor\frac{n}{p^k}\rfloor,(p^k\le n)
⌊p1n⌋+⌊p2n⌋+⌊p3n⌋+⋯+⌊pkn⌋,(pk≤n)
代码
#include <iostream>
#include <cstring>
#define MOD 10000
using namespace std;
const int N = 1e5 + 5;
int p[N], t;
bool s[N];
int sum[N]; //sum[i]表示第i个质数p[i]出现的次数(指数)
struct HP { //四位压缩高精度
int l[2000];
int len;
HP() {
memset(l, 0, sizeof(l));
len = 0;
}
void print() {
printf("%d",l[len]);
for (int i = len - 1; i > 0; i--)
printf("%04d", l[i]);
printf("\n");
}
};
HP operator*(const HP &a, const int &b) { //高精乘低精
HP c;
c.len = a.len;
int x = 0;
for (int i = 1; i <= c.len; i++) {
c.l[i] = a.l[i] * b + x;
x = c.l[i] / MOD;
c.l[i] %= MOD;
}
while (x) {
c.len++;
c.l[c.len] = x % MOD;
x /= MOD;
}
return c;
}
void prime(int n) { //筛n以内的素数
int i, j;
for (i = 2; i <= n; i++) {
if (!s[i]) p[++t] = i;
for (j = 1; p[j] <= n / i; j++) {
s[i * p[j]] = 1;
if (i % p[j] == 0) break;
}
}
}
int get(int n, int p) { //n!中p这个质数出现了几次
int res = 0;
while (n) {
res += n / p;
n /= p;
}
return res;
}
int main() {
int a, b;
cin >> a >> b;
prime(a);
for (int i = 1; i <= t; i++) {
int pr = p[i];
sum[i] = get(a, pr) - get(b, pr) - get(a - b, pr); //相当于约分操作
}
HP res;
res.l[1] = 1;
res.len = 1;
for (int i = 1; i <= t; i++)
for (int j = 1; j <= sum[i]; j++)
res = res * p[i]; //做乘法
res.print();
return 0;
}