求组合数
加法递推 – O( n 2 n^2 n2)
Problem A 求组合数Ⅰ
给定 n 组询问,每组询问给定两个整数 a , b , 请你输出 C a b m o d ( 1 0 9 + 7 ) C^b_a \ \ mod ~~(10^9 + 7) Cab mod (109+7) 的值
1 <= n <= 100000, 1 <= b <= a <= 2000
思路
- 假如我们有
a
个苹果,其中有1
个是青苹果,剩下的a - 1
个均是红苹果,现在我们要从中选取b
个苹果,问有多少种选法- (1)包含青苹果,那么剩下的
b - 1
苹果,就从a - 1
中选取 - (2)不包含青苹果,那么选取的
b
个苹果就从a - 1
中选取 - 那么我们则有 C a b = C a − 1 b − 1 + C a − 1 b \color{Orange}C^b_a =C^{b-1}_{a-1} + C^b_{a-1} Cab=Ca−1b−1+Ca−1b
- (1)包含青苹果,那么剩下的
- 如果每次询问我们都重新计算组合值,那么对于整体的时间复杂度而言太高,故此我们需要将计算过的数值存储下来,以便后序使用
代码模板
const int N = 2010, mod = 1e9 + 7;
int n, m; // C(n, m) = C(n − 1, m - 1) + C(n − 1, m);
int c[N][N]; //表示从 a 个苹果中选 b 个的方案
void init()
{
c[1][0] = c[1][1] = 1; // 边界条件 C(n , 0) = C(n , n) = 1;
for(int i = 2; i < N; i ++ )
{
c[i][0] = 1;
for(int j = 1; j < N; j ++ )
c[i][j] = (1ll * c[i - 1][j - 1] + c[i - 1][j]) % mod;
}
}
乘法递推 – O( n l o g n nlogn nlogn)
Problem B 求组合数Ⅱ
给定 n 组询问,每组询问给定两个整数 a , b , 请你输出 C a b m o d ( 1 0 9 + 7 ) C^b_a \ \ mod ~~(10^9 + 7) Cab mod (109+7) 的值
1 <= n <= 100000, 1 <= b <= a <= 10^5
如果不熟悉逆元求解的,可以点开这里食用 - > 乘法逆元(inverse element)及四大相关求法详解(含证明)
思路
-
利用公式 C n m = n ! ( n − m ) ! × m ! \color{Orange}C^m_n = \frac{n~!}{(n ~-~ m)~!~×~ m~!} Cnm=(n − m) ! × m !n !
-
由于模数
mod = 1e9 + 7
为质数,可以用费马小定理求逆元,故我们可以先预处理出所有阶乘取模的余数fact[N]
,阶乘取模的逆元invfact[N]
代码模板
typedef long long LL;
const int N = 1e5 + 10, mod = 1e9 + 7;
LL fact[N];// 阶乘 ,分子
LL invfact[N]; // 阶乘逆元,即分母
LL qmi(LL a, LL k, LL p)
{
LL res = 1;
while(k)
{
if(k & 1) res = (LL) res * a % p;
a = (LL) a * a % p;
k >>= 1;
}
return res;
}
void init()
{
fact[0] = invfact[0] = 1;
for(int i = 1; i < N; i ++ )
{
fact[i] = (LL) fact[i] * i % mod; // n ! = n *(n - 1) *... * 1;
invfact[i] = (LL) qmi(fact[i], mod - 2, mod) % mod;
}
}
LL C(LL a, LL b)
{
if(a < b) return 0;
if(b == 0 || a == b) return 1;
return 1ll * fact[a] * invfact[a - b] % mod * invfact[b] % mod;
}
Lucas 定理 – O( l o g p n × p l o g p log_p^n × p logp logpn×plogp)
Problem C 求组合数Ⅲ
给定 n 组询问,每组询问给定三个整数a, b ,p,其中 p 是质数,请你输出 C a b m o d p C^b_a~ mod~p Cab mod p 的值
1 <= n <= 20,1 <= b <= a <= 10^18 , 1 <= p <= 10^5
思路
Lucas 定理:
若 p 为质数,则对于任意的整数
1
<
=
m
<
=
n
1<= m < = n
1<=m<=n, 有:
(
n
m
)
=
(
n
m
o
d
p
m
m
o
d
p
)
∗
(
⌊
n
p
⌋
⌊
m
p
⌋
)
(
m
o
d
p
)
\dbinom{n}{m} =\dbinom{n ~ mod~ p}{m ~ mod ~ p} * \dbinom{\lfloor{\frac{n}{p}}\rfloor}{\lfloor{\frac{m}{p}}\rfloor} ~(mod~p)
(mn)=(m mod pn mod p)∗(⌊pm⌋⌊pn⌋) (mod p)
- 利用利用快速幂求出 ( n m o d p m m o d p ) \dbinom{n ~ mod~ p}{m ~ mod ~ p} (m mod pn mod p) 组合数
- 递归 Lucas 求解 ( ⌊ n p ⌋ ⌊ m p ⌋ ) \dbinom{\lfloor{\frac{n}{p}}\rfloor}{\lfloor{\frac{m}{p}}\rfloor} (⌊pm⌋⌊pn⌋)
代码模板
typedef long long LL;
LL qmi(LL a, LL k, LL p)
{
LL res = 1;
while(k & 1)
{
if(k & 1) res = (LL) res * a % p;
a = (LL) a * a % p;
k >>= 1;
}
return res;
}
LL C(LL a, LL b, LL p)
{
if(a < b) return o;
LL up = 1, down = 1;
for(int i = a, j = 1; j <= b; i --, j ++ ) //
{
up = (LL) up * i % p;
down = (LL) down * j % p;
}
return up * (LL) qmi(down, p - 2, p) % p;
}
LL Lucas(LL a, LL b, LL p)
{
if(a < p && b < p) return C(a, b, p);
return (LL) C(a % p, b % p, p) * Lucas(a/p, b/p, p) % p;
}
求大组合数 (高精度)
Problem D 求组合数Ⅳ
给定两个整数 a , b,求 C a b C^b_a Cab 的值
注意结果可能很大,需要用高精度计算
1 <= b <= a <= 5000
思路
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用
-
筛法 求出范围内的所有质数
-
通过 C a b = a ! b ! × ( a − b ) ! \color{Orange} C^b_a = \frac{a~!}{b~!×(a - b)~!} Cab=b !×(a−b) !a ! 求出每个质因子的次数
-
知 识 点 \color{Turquoise}知识点 知识点:求
a !
质因数的次数(分解质因数) = p 1 α 1 ∗ p 2 α 2 ∗ . . . ∗ p k α k p_1^{\alpha_1}*p_2^{\alpha_2}*...*p_k^{\alpha_k} p1α1∗p2α2∗...∗pkαk
求 a ! 分解每个 p的次数 :
c n t ( a ! ) = ⌊ a p ⌋ 下 取 整 + ⌊ a p 2 ⌋ 下 取 整 + ⌊ a p 3 ⌋ 下 取 整 + ⋅ ⋅ ⋅ cnt(a~!) = \lfloor{\frac{a}{p}\rfloor}^{下取整} +\lfloor{\frac{a}{p^2}\rfloor}^{下取整} +\lfloor{\frac{a}{p^3}\rfloor}^{下取整} + ··· cnt(a !)=⌊pa⌋下取整+⌊p2a⌋下取整+⌊p3a⌋下取整+⋅⋅⋅= p 的 倍 数 中 ∈ [ 1 , a ] 的 个 数 + p 2 的 倍 数 中 ∈ [ 1 , a ] 的 个 数 + … ~~=p的倍数中^{∈[1,a]的个数}+p^2的倍数中^{∈[1,a]的个数}+… =p的倍数中∈[1,a]的个数+p2的倍数中∈[1,a]的个数+…
为什么这样计算 p 的次数只
+ 1
,而不是+ k
,因为 a 中 p k p ^k pk 低阶项的次数已经被计算过了,比如 p 3 p^3 p3 既是 p 的倍数也是 p 2 p^2 p2 的倍数例如,当我们求 8 ! = 1 × 2 × 3 × 4 × 5 × 6 × 7 × 8 8 ! = 1 ×2 ×3 ×4 ×5 ×6 ×7 ×8 8!=1×2×3×4×5×6×7×8, 其中 2 的个数
- 1 ~ 8 之间 含 2 一次的数有 2,4,6,8 四个 2
- 含 2 的二次的数有 4,8 ,但 4, 8 中 2 的一次方此前已经记过 一次了,故只加二次方中的 两个 2
- 含 2 的三次方的数有 8,但 8 中 的 一次,二次方都被记过一次了,故只加三次方中的 一个 2
同理可以类推到 p k p^k pk 它的幂次中的前 k - 1个 p 在 计算 p 1 p^1 p1 ~ p k − 1 p^{k-1} pk−1 次数都已经算过了 1次了
-
-
利用高精度乘法将所有质因子相乘
- 故对于
C
a
b
=
a
!
b
!
×
(
a
−
b
)
!
C^b_a = \frac{a~!}{b~!×(a - b)~!}
Cab=b !×(a−b) !a ! 中 质因子的次数 为
∏ i p i 分 子 次 数 和 − 分 母 次 数 和 \color{Orange}{\prod_ip_i^{分子次数和 - 分母次数和}} i∏pi分子次数和−分母次数和
代码模板
int primes[N], cnt; // prime[] 存储所有素数
bool st[N];// st[x] 存储 x 是否被筛掉
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) // 计算 质因子
{
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;
while(c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
int sum[N];// 储存c[a][b] 中某个质因子p 的次数
get_primes(a); // 预处理范围内的所有质数
for(int i = 0; i < cnt; i ++ ) // 得到C[a][b]中p的次数:分子的次数 - 分母的次数
{
int p = primes[i];
sum[i] = get(a, p) - get(a - b, p) - get(b, p);
}
vector<int> res;
res.push_back(1);// 放入一个 1,与 质因数高精度相乘
for(int i = 0; i< cnt; i ++ ) // 用高精度将所有质因子相乘
for(int j = 0; j < sum[i]; j ++ )
res = mul(res, primes[i]);
for(int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);// 输出组合值
以上内容尚未完全,随着今后学习的推进,我会继续对其进行补充与完善。另外,大家如果觉得我写的还行的话,还请赠予我一个可爱的赞,你的赞对于我是莫大的支持