求组合数
Method 1 预处理出所有组合数的值
数据范围
询问次数 T = 1 0 5 , 0 ≤ a , b ≤ 1 0 4 T=10^5 ,0\le a,b\le 10^4 T=105,0≤a,b≤104
求解方法
根据递推式 C a b = C a − 1 b + C a − 1 b − 1 , ( 0 ≤ a , b ≤ 1 0 4 ) C_{a}^{b}=C_{a-1}^{b}+C_{a-1}^{b-1},(0\le a,b\le 10^4) Cab=Ca−1b+Ca−1b−1,(0≤a,b≤104)
从实际意义出发理解这个式子,从 a a a 个苹果中选 b b b 个苹果的方案数可以分成两类,从所有苹果中挑出一个苹果 A A A 把所有方案分为包含 A A A 和不包含 A A A 的两类,如果包含 A A A ,方案数是 C a − 1 b − 1 C_{a-1}^{b-1} Ca−1b−1 ,如果不包含 A A A ,方案数是 C a − 1 b C_{a-1}^{b} Ca−1b
Code
void init() {
for(int i = 0; i < N; i++) { //注意i和j都要从0开始遍历,因为C[0][0]也是有定义的
for(int j = 0; j <= i; j++) {//因为C[a][b]的a一定大于等于b所以遍历到i就可以
if(j == 0) c[i][j] = 1;
else c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
}
}
}
时间复杂度
O ( n 2 ) O(n^2) O(n2)
Method 2 预处理阶乘和阶乘的逆元
数据范围
询问次数 T = 1 0 5 , 0 ≤ a , b ≤ 1 0 5 T=10^5 ,0\le a,b\le 10^5 T=105,0≤a,b≤105
求解方法
由于
C
a
b
=
a
!
(
b
−
a
)
!
b
!
C_{a}^{b}=\frac{a!}{(b-a)!b!}
Cab=(b−a)!b!a!
预处理
i
i
i 的阶乘
m
o
d
p
mod\ p
mod p 的值 ,
i
i
i 的阶乘的逆元
m
o
d
p
mod \ p
mod p 的值
所以
C
a
b
=
f
a
c
t
(
a
)
×
i
n
f
a
c
t
(
b
−
a
)
×
i
n
f
a
c
t
(
b
)
C_{a}^{b}=fact(a)\times infact(b-a)\times infact(b)
Cab=fact(a)×infact(b−a)×infact(b)
其中, f a c t fact fact 表示 i i i 的阶乘 i n f a c t infact infact 表示 i i i 的阶乘的逆元
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 7, M = N * 2;
const long long mod = 1e9 + 7;
int fact[N], infact[N];
ll qmi(ll a, ll k, ll p) {
ll res = 1;
while(k) {
if(k & 1) res = res * a % p;
k >>= 1;
a = a * a % p;
}
return res;
}
int main() {
//初始化i的阶乘
fact[0] = infact[0] = 1;
for(int i = 1; i < N; i++) {
fact[i] = (ll)fact[i - 1] * i % mod;
infact[i] = (ll)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
int n;
scanf("%d", &n);
while(n--) {
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", (ll)fact[a] * infact[a - b] % mod * infact[b] % mod);
}
return 0;
}
时间复杂度
O ( n l o g n ) O(nlogn) O(nlogn)
Method 3 卢卡斯定理 Lucas
数据范围
n n n 是询问次数, p p p 是模数
1
≤
n
≤
20
1≤n≤20
1≤n≤20
1
≤
b
≤
a
≤
1
0
18
1≤b≤a≤10^{18}
1≤b≤a≤1018
1
≤
p
≤
1
0
5
1≤p≤10^5
1≤p≤105
求解方法
C a b ≡ ( C a m o d p b m o d p × C a ÷ p b ÷ p ) m o d p C_{a}^{b}\equiv (C_{a\mod {p}}^{b\mod {p}}\times C_{a\div p}^{b\div p}) \ mod \ p Cab≡(Camodpbmodp×Ca÷pb÷p) mod p
卢卡斯定理的证明
时间复杂度
O ( l o g n N × p × l o g p ) O(log_n^N\times p\times logp) O(lognN×p×logp)
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int p;
ll qmi(ll a, ll k) {
ll res = 1;
while(k) {
if(k & 1) res = res * a % p;
a = a * a % p;
k >>= 1;
}
return res;
}
ll C(ll a, ll b) {
ll res = 1;
for(int i = 1, j = a; i <= b; i++, j--) {
res = res * j % p;
res = res * qmi(i, p - 2) % p;
}
return res;
}
ll 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;
cin >> n;
while(n--) {
ll a, b;
cin >> a >> b >> p;
cout << lucas(a, b) << endl;
}
return 0;
}
Method 4 大数运算
数据范围
1 ≤ b ≤ a ≤ 5000 1≤b≤a≤5000 1≤b≤a≤5000
求解方法
根据定义
C a b = a ( a − 1 ) ( a − 2 ) . . . ( a − b + 1 ) b ( b − 1 ) ( b − 2 ) . . . 2 × 1 C_{a}^{b}=\frac{a(a-1)(a-2)...(a-b+1)}{b(b-1)(b-2)...2\times1} Cab=b(b−1)(b−2)...2×1a(a−1)(a−2)...(a−b+1)
对其分解质因数
p 1 α 1 p 2 α 2 . . . p k α k p_1^{\alpha_1}p_2^{\alpha_2}...p_k^{\alpha_k} p1α1p2α2...pkαk
变成只有乘法的形式,实现高精度乘法
具体步骤如下
- 筛素数 筛出 1 ∼ 5000 1\sim 5000 1∼5000 内的素数
- 求每个质数的次数
使用 a ! = ⌊ a p ⌋ + ⌊ a p 2 ⌋ + ⌊ a p 3 ⌋ + . . . a!=\lfloor\frac{a}{p}\rfloor+\lfloor\frac{a}{p^2}\rfloor+\lfloor\frac{a}{p^3}\rfloor+... a!=⌊pa⌋+⌊p2a⌋+⌊p3a⌋+...
- 用高精度乘法把所有质因子乘到一块去
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 7, M = N * 2;
int primes[N], cnt;
int sum[N];//求p的次数
bool st[N];
void get_primes(int n) {
for(int i = 2; i <= n; i++) {
if(!st[i]) primes[cnt++] = i;
for(int j = 0; primes[j] <= n / i; j++) {
st[primes[j]*i] = true;
if(i % primes[j] == 0) break;
}
}
}
int get(int n, int p) { //求n的阶乘中包含p的个数
int res = 0;
while(n) {
res += n / p;
n /= p;
}
return res;
}
vector<int> mul(vector<int> a, int b) {
vector<int> c;
int t = 0;
for(int i = 0; i < a.size(); i++) {
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while(t){
c.push_back(t%10);
t/=10;
}
return c;
}
int main() {
int a, b;
cin >> a >> b;
get_primes(a);
for(int i = 0; i < cnt; i++) {
int p = primes[i];
sum[i] = get(a,p) - get(b,p) - get(a - b,p);
}
//高精度乘法
vector<int> ans;
ans.push_back(1);
for(int i = 0; i < cnt; i++) {
for(int j = 0; j < sum[i]; j++) {
ans = mul(ans, primes[i]);
}
}
for(int i = ans.size() - 1; i >= 0; i--) printf("%d",ans[i]);
return 0;
}
一些例题
卡特兰数
把每一个序列转化为一条路径, 0 表示向右走一格,1 表示向上走一格,那么任何前缀中 0 的个数不少于 1 的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。
下图中,表示从 ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的路径,在绿线及以下表示合法,若触碰红线即不合法。
把每一个序列转化为一条路径, 0 表示向右走一格,1 表示向上走一格,那么任何前缀中 0 的个数不少于 1 的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。
下图中,表示从 ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的路径,在绿线及以下表示合法,若触碰红线即不合法。
-
从 ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的路径一共有 2 n 2n 2n 种走法,在其中选择 n n n 种走法,总共的方案数就是 C 2 n n C^{n}_{2n} C2nn
-
对于任何一个经过红颜色边的路径,把第一次经过红颜色边之后的路径做轴对称,终点一定是 ( n − 1 , n + 1 ) (n-1,n+1) (n−1,n+1) ,不合法的方案数为, C 2 n n − 1 / C 2 n n + 1 C^{n-1}_{2n}/C^{n+1}_{2n} C2nn−1/C2nn+1
-
最终结果为 a n s = C 2 n n − C 2 n n − 1 = C 2 n n − 1 n + 1 ans=C^{n}_{2n}-C^{n-1}_{2n}=\frac{C^{n-1}_{2n}}{n+1} ans=C2nn−C2nn−1=n+1C2nn−1
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const long long mod = 1e9 + 7;
ll qmi(ll a, ll k, ll p) {
ll res = 1;
while(k) {
if(k & 1) res = res * a % p;
a = a * a % p;
k >>= 1;
}
return res;
}
int main() {
ll n;
cin >> n;
ll res = 1;
ll a = 2 * n;
ll b = n;
for(int i = a; i >= a - b + 1; i--) res = res * i % mod;
for(int i = 1; i <= b; i++) res = res * qmi(i, mod - 2, mod) % mod;
res = (res * (n + 1))% mod;
cout << res;
return 0;
}