组合数学
1 计数原理和方法
1.1 加法原理
完成一件事情有
1.2 乘法原理
完成一件事情有
1.3 排列与组合
1.3.1 排列
从
的排列,用
1.3.2 组合
从
的组合,用
1.3.3 常用策略与方法
1.3.3.1 特殊优先
将特殊要求的优先考虑,普通情况靠后考虑。
1.3.3.2 捆绑法
将要求相邻的元素捆绑为一个整体,与其他元素算出排列,再在整体内部进行排列。
1.3.3.3 插空法
插空法用于处理不相邻问题。把要求不相邻的元素放一边,算出其他元素的排列,再把不相邻的元素插入已经排好的元素之间的空位。
插空法常用结论:
- 将一个长度为
的序列划分为 非空序列的方案数为 。 - 将一个长度为
的序列划分为 可空序列的方案数为 。
1.3.3.4 倍缩法
对于某几个元素顺序一定的排列,先把这几个元素和其他的一起排列,再除以这几个元素间的全排列数。
1.3.3.5 环问题策略
把环破为链,而由于圆没有首尾之分。所以例如
一般的,
1.3.3.6 错排问题
错排问题指的是有
这是一个递推问题。将
第一步,考虑放下第
第二步,考虑放下第
- 将它放在
位置,此时剩下的 个元素有 种方法。 - 将它不放在
位置,这时对于除 外的 个元素有 种方法。
综上,
边界为
1.3.4 组合数取模
1.3.4.1 递推求解
递推式明显可得:
时间复杂度
1.3.4.2 公式法求解
利用
用两个数组存储模
补:阶乘的逆元可以递推计算
1.3.4.3 Lucas 定理
公式:
其中第一项继续用 Lucas 定理递归求解,第二项直接用公式法求解即可。
时间复杂度为
完整代码:
int g[Maxn], f[Maxn], p;
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) {
res = (res * a) % p;
}
a = (a * a) % p;
b >>= 1;
}
return res;
}
void init() {
f[0] = g[0] = 1;
for(int i = 1; i < p; i++) {
f[i] = (f[i - 1] * i) % p;
g[i] = (g[i - 1] * qpow(i, p - 2)) % p;
}
}
int getc(int n, int m) {
if(n < m) return 0;
return f[n] * g[n - m] % p * g[m] % p;
}
int lucas(int n, int m) {
if(m == 0) return 1;
if(n < m) return 0;
return lucas(n / p, m / p) * getc(n % p, m % p) % p;
}
1.3.4.4 小结
比较三种方法:
方法 | 复杂度 | 用途 |
---|---|---|
递推法 | 当 |
|
公式法 | 预处理 |
当 |
Lucas 定理 | 当 |
2 中国剩余定理(CRT)
2.1 简介
中国剩余定理,简称 CRT,用来解如下的一元同余方程组的最小非负数解
(其中
2.2 算法流程
- 计算所有模数的乘积
。 - 对于第
个方程,计算出 ,并求出 在模 意义下的逆元。 - 方程的解为
。
复杂度
2.2.1 算法证明
虽然没用,但是理解了更好记忆吧。
先证明
分情况讨论:
当
当
综上,对于任何一个
而
算法证毕。
2.2.2 代码
直接模拟即可。
int exgcd(int a, int b, int &x, int &y) {//扩欧求逆元
if(b == 0) {
x = 1, y = 0;
return a;
}
int ret = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - a / b * y;
return ret;
}
int CRT(int m[], int r[]) {
int M = 1, ans = 0;
for(int i = 1; i <= n; i++) {
M *= m[i];
}
for(int i = 1; i <= n; i++) {
int c = M / m[i], x, y;
int d = exgcd(c, m[i], x, y);
ans = (ans + r[i] * c * x % M) % M;
}
ans = (ans % M + M) % M;
return ans;
}
2.3 扩展中国剩余定理(exCRT)
扩展中国剩余定理,简称 exCRT,用来解如下的一元同余方程组的最小非负数解
其中
2.3.1 算法过程
考虑前两个方程
转化为不定方程即
所以
由裴蜀定理可知当
由扩欧,得到方程特解为
于是通解就是
所以
根据上式,前两个方程就等价于合并为一个方程:
其中
于是
2.3.2 代码
int exgcd(int a, int b, int &x, int &y) {
if(b == 0) {
x = 1, y = 0;
return a;
}
int ret = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - a / b * y;
return ret;
}
int exCRT(int m[], int r[]) {
int m1, m2, r1, r2, p, q;
m1 = m[1], r1 = r[1];
for(int i = 2; i <= n; i++) {
m2 = m[i], r2 = r[i];
int d = exgcd(m1, m2, p, q);
if((r2 - r1) % d != 0) return -1;
p = p * (r2 - r1) / d;
p = (p % (m2 / d) + (m2 / d)) % (m2 / d);
r1 = m1 * p + r1;
m1 = m1 * m2 / d;
}
return (r1 % m1 + m1) % m1;
}
3 扩展卢卡斯定理(exLucas)
虽然这应该是放在 1.3.4 中的,但是他值得单独拿出来。
我愿称之为数论与组合的集大成之算法,他使用到的包括 CRT、exgcd、快速幂、分解质因数。
3.1 问题
求
3.2 求解
我们逐步分解问题求解。
3.2.1 CRT
将
此时显然
3.2.2 组合数模素数幂
我们继续看上面的式子:
我们回顾组合数的公式
我们可以将
此时分母就互质了,直接求逆元即可。
但是还有一个问题:如何求阶乘模数。
3.2.3 阶乘模素数幂
我们继续看下面的式子:
我们先算
举个例子:
写出来:
把当中所有含
可以看出上式分为三个部分:第一个部分是
第二个部分是
第三个部分是
写成如下形式更加显然。
暴力求出
最后再乘上
然后三部分的乘积就是
直接看代码:
int fac(int n, int p, int mod) {//阶乘模素数幂
if(!n) return 1;
int ans = 1;
for(int i = 1; i < mod; i++) {
if(i % p) {
ans = ans * i % mod;
}
}
ans = qpow(ans, n / mod, mod);
for(int i = 1; i <= n % mod; i++) {
if(i % p) {
ans = ans * i % mod;
}
}//全部是第三部分
return ans * fac(n / p, p, mod)/*递归求解第二部分*/ % mod;
}
3.2.4 回代求解——组合数模素数幂
回到 3.2.2 的式子
现在就可以直接转化为代码了:
int C(int n, int m, int p, int mod) {//组合数模素数幂
if(n < m) {
return 0;
}
int f1 = fac(n, p, mod), f2 = fac(m, p, mod), f3 = fac(n - m, p, mod);
//求出除掉 p 后的阶乘值
int cnt = 0;//k1-k2-k3
for(int i = n; i; i /= p) {
cnt += i / p;
}
for(int i = m; i;i i /= p) {
cnt -= i / p;
}
for(int i = n - m; i; i /= p) {
cnt -= i / p;
}
return f1 * inv(f2, mod) % mod * inv(f3, mod) % mod * qpow(p, cnt, mod) % mod;
//利用逆元计算
}
3.2.5 回代求解——CRT
直接带入计算即可。
代码直接看模板吧。
3.3 代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef long long LL;
const int Maxn = 2e6 + 5;
int qpow(int a, int b, int mod) {//快速幂
int res = 1;
while(b) {
if(b & 1) {
res = (res * a) % mod;
}
a = (a * a) % mod;
b >>= 1;
}
return res;
}
int exgcd(int a, int b, int &x, int &y) {//扩欧
if(!b) {
x = 1, y = 0;
return a;
}
int ret = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - a / b * y;
return ret;
}
int inv(int a, int p) {//利用扩欧求逆元
int x, y;
int ret = exgcd(a, p, x, y);
return (x % p + p) % p;
}
int fac(int n, int p, int mod) {//阶乘模素数幂
if(!n) return 1;
int ans = 1;
for(int i = 1; i < mod; i++) {
if(i % p) {
ans = ans * i % mod;
}
}
ans = qpow(ans, n / mod, mod);
for(int i = 1; i <= n % mod; i++) {
if(i % p) {
ans = ans * i % mod;
}
}//全部是第三部分
return ans * fac(n / p, p, mod)/*递归求解第二部分*/ % mod;
}
int C(int n, int m, int p, int mod) {//组合数模素数幂
if(n < m) {
return 0;
}
int f1 = fac(n, p, mod), f2 = fac(m, p, mod), f3 = fac(n - m, p, mod);
//求出除掉 p 后的阶乘值
int cnt = 0;//k1-k2-k3
for(int i = n; i; i /= p) {
cnt += i / p;
}
for(int i = m; i; i /= p) {
cnt -= i / p;
}
for(int i = n - m; i; i /= p) {
cnt -= i / p;
}
return f1 * inv(f2, mod) % mod * inv(f3, mod) % mod * qpow(p, cnt, mod) % mod;
//利用逆元计算
}
int CRT(int m[], int n[], int l) {//普通 CRT
int M = 1, ans = 0;
for(int i = 1; i <= l; i++) {
M *= m[i];
}
for(int i = 1; i <= l; i++) {
int t = M / m[i];
int p = inv(t, m[i]);
ans = (ans + n[i] * t % M * p % M) % M;
}
ans = (ans % M + M) % M;
return ans;
}
int a[Maxn], b[Maxn], cnt;
int exLucas(int n, int m, int p) {
int w = sqrt(p);
for(int i = 2; i <= w; i++) {
int tmp = 1;
while(p % i == 0) {//直接求 p^k
p /= i;
tmp *= i;
}
if(tmp > 1) {
a[++cnt] = C(n, m, i, tmp);//求 Cnm mod p^k
b[cnt] = tmp;
}
}
if(p > 1) {
a[++cnt] = C(n, m, p, p);
b[cnt] = p;
}
return CRT(b, a, cnt);//CRT 求解
}
int n, m, p;
signed main() {
ios::sync_with_stdio(0);
cin >> n >> m >> p;
cout << exLucas(n, m, p);
return 0;
}
长达 119 行,甚至超过了普通线段树。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律