数论补完计划 Part5 组合计数
排列数与组合数
排列组合基础
对于一个正整数
小学时我们就知道,用
排列数
上了高中,我们又知道,从
组合数
记不住组合数公式的看这张图:

还有特殊情况,例如
一个重要的恒等式是:
排列组合扩展
比如说二项式定理,这个非常的好用,尤其是当你需要计算
来快捷得到。注意
跟二项式定理密切联系的还有一个杨辉三角,它长这个样子:
发现所有的二项式系数都可以在对应行找到。因此可以说,杨辉三角第
杨辉三角还有许多有趣的性质,例如将第
即
Lucas 定理
用于组合数取模问题,公式其实很简洁:
其中 暗示扩展),除法为整除。而当
如果考虑递归,我们其实发现它和 不会)。它经常和数位DP放在一起考。
虽然它简化了计算,但是它本质上还是需要计算组合数的值。因此需要维护模意义下的阶乘和阶乘的逆元,或者你还可以暴力求解,适用于模数易变的情况。
// 暴力求解组合数int C(int n, int m, int p) { int res = 1; for (int i = 1, j = n; i <= m; i++, j--) { res = ((ll) res % p * j % p * inv(i, p) % p) % p; } return res;}
int lucas(int n, int m, int p) { if (m > n) return 0; // 必须特判,否则 RE+WA if (n < p && m < p) return C(n, m, p) % p; return (lucas(n / p, m / p, p) % p * lucas(n % p, m % p, p) % p) % p;}// 预处理int lucas(int n, int m, int p) { if (n < m) return 0; // 必须特判 if (n < p && m < p) return (fac[n] % p * infac[m] % p * infac[n - m] % p) % p; return (lucas(n / p, m / p, p) % p * lucas(n % p, m % p, p) % p) % p;}
void init(int lim) { fac[0] = infac[0] = 1; for (int i = 1; i <= lim; i++) { fac[i] = (fac[i - 1] * i % p) % p; infac[i] = (infac[i - 1] * inv(i, p) % p) % p; }}无论你选用哪一种,都不要忘了对特殊情况的特判,例如
扩展 Lucas 定理
回收伏笔
既然
发现可以使用朴素的中国剩余定理合并(不同质数之间显然互质),前提是我们得知道
根据组合数的阶乘定义式得到:
此时因为除去了因子
例 3.1
求解
的值
即
发现原式分为三个部分:
假设每一个完整组的乘积模
递归的下一层就是求解
#include <bits/stdc++.h>using namespace std;
typedef long long ll;typedef pair<ll, ll> Equation;
ll exgcd(ll a, ll b, ll &x, ll &y) { if (!b) { x = 1, y = 0; return a; } ll d = exgcd(b, a % b, y, x); y -= a / b * x; return d;}
ll inv(ll a, ll p) { ll ans, tmp; exgcd(a, p, ans, tmp); return (ans % p + p) % p;}
ll qmul(ll a, ll b, ll p) { ll res = 0; while (b) { if (b & 1) res = (res + a) % p; a = (a + a) % p; b >>= 1; } return res % p;}
ll qpow(ll a, ll b, ll p) { ll res = 1; while (b) { if (b & 1) res = (res % p * a % p) % p; a = (a * a) % p; b >>= 1; } return res % p;}
ll mul(initializer_list<ll> args, ll p) { // 很方便的变长参数列表求连乘,int128 防止爆范围 __int128 res = 1; for (ll i: args) { res = (res % p * i % p) % p; } return (ll) (res % p);}
ll CRT(vector<Equation> &v) { // 中国剩余定理部分 ll M = 1; ll res = 0; for (Equation eq: v) M *= eq.second; for (Equation i: v) { ll Mi = M / i.second; res = (res + qmul(qmul(i.first, Mi, M), inv(Mi, i.second), M)) % M; } return res % M;}
ll resolve(ll a, ll p, ll ppow) { // 递归过程求解,即实现例3.1 if (!a) return 1; // 1的阶乘模质数当然无论如何都是1 ll s = 1; for (ll i = 1; i <= ppow; i++) { if (i % p) s = mul({s, i}, ppow); // 完整组循环节连乘 } s = qpow(s, a / ppow, ppow); // 对完整组的积求幂 for (ll i = a / ppow * ppow + 1; i <= a; i++) { if (i % p) s = mul({s, i}, ppow); // 对不完整组求积 } return mul({s, resolve(a / p, p, ppow)}, ppow); // 递归过程}
ll getA(ll n, ll m, ll p, ll ppow) { // 获取 CRT 方程组中的 a ll x = 0, y = 0, z = 0; // 统计 n, m, n - m 分别是 p 的多少次幂 for (ll i = n; i; i /= p) x += i / p; for (ll i = m; i; i /= p) y += i / p; for (ll i = n - m; i; i /= p) z += i / p; // 套公式 return mul({ qpow(p, x - y - z, ppow), resolve(n, p, ppow), inv(resolve(m, p, ppow), ppow), inv(resolve(n - m, p, ppow), ppow) }, ppow);}
ll exLucas(ll n, ll m, ll p) { // 分解质因数 vector<Equation> E; for (ll i = 2; i * i <= p; i++) { if (p % i == 0) { // 统计对应质数以及它的幂 ll c = 1; while (p % i == 0) { c *= i; p /= i; } E.emplace_back(getA(n, m, i, c), c); } } if (p > 1) E.emplace_back(getA(n, m, p, p), p); // 最后一个质数 return CRT(E);}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
ll n, m, p; cin >> n >> m >> p; cout << exLucas(n, m, p) << endl;
return 0;}其中 getA(n, m, p, ppow) 用来求解 resolve(a, p, ppow) 实现求解
Lucas 定理的实际应用
鉴于
洛谷 P7976 [Stoi2033] 园游会
题目地址:P7976
题目难度:提高+/省选-
设
,给定 ,求: 对
取模。 对于
的数据, 。
根据
我们把
,保证 的情况下, ,此时贡献为 , 只能枚举 ,贡献为 ,此时 ,贡献为
因此,真正能对答案产生实质性贡献的就是
洛谷 P6669/BZOJ 4737 [清华集训2016] 组合数问题
题目地址:P6669
题目难度:省选/NOI-
题目来源:清华集训 2016
给定
和 ,对于所有的 有多少对 满足 是 的倍数。答案对 取模。 对于
的测试点, , ,且 是一个质数。
读题,倍数必定满足关系
布尔型维护当前是否已经存在
题解同步于本站
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
部分内容可能已过时