Appearance
扩展欧几里得算法
用于求解
将上式展开:
因为只要求找到一组合法解,所以可以构造:
所以,在辗转相除法求解
递归边界是
解得:
感性地理解,每次
那么,
所以取
上述过程只是求出了
点击展开代码
cpp
void exgcd(int a, int b, int &x, int &y) {
int t;
if (!b) {
x = 1, y = 0;
} else {
exgcd(b, a % b, x, y);
t = x;
x = y;
y = t - (a / b) * y;
}
}考虑通解:
因为
已知
用
所以
综上,原方程关于
求解二元一次方程整数解
裴蜀引理:
有整数解当且仅当 。
广义裴蜀引理:
有整数解当且仅当
根据「裴蜀引理」,二元一次方程
使用 exgcd 求出
代入原方程,可得:
原方程的通解为:
求解线性同余方程
对于线性同余方程
点击展开代码
cpp
int tyfc(int a, int b, int c) {
b %= c;
int x, y;
if (b % gcd(a, c))
return -1;
exgcd(a, c, x, y);
int g = gcd(a, c);
x *= b / g;
c /= g;
x = (x % c + c) % c;
return x;
}欧拉定理
费马小定理:
,其中 且 。
费马小定理是欧拉定理在
扩展欧拉定理
用于大指数取模。
欧拉降幂
求
结论是一个数自取
简单证明:
是偶数
所以至少是两次缩小一半规模。
卢卡斯定理
对于质模数
Lucas 定理揭示了,组合数
即:
对于
所以时间复杂度:
点击展开代码
cpp
int C(int x, int y, int mod) {
auto calc = [](int x, int y, int mod) -> int {
if (x < y)
return 0;
if (x == 0 || y == 0)
return 1;
return 1LL * fac[x] * inv[y] % mod * inv[x - y] % mod;
};
if (x < y)
return 0;
int res = 1;
while (x || y) {
int X = x % mod, Y = y % mod;
res = 1LL * res * calc(X, Y, mod) % mod;
x /= mod;
y /= mod;
}
return res;
}扩展卢卡斯定理
若模数不为质数,令
根据 crt,等价于求解
考虑求解
令
则原式可以写成
此时,问题转换成求
所以 exlucas 本质是解决了阶乘对质数幂取模的问题。
Wilson 定理:
。 推广:
。 推论:
。特别地,模 时完整单位块积为 。
对于
可以发现,实际上 lucas 就是模数为
对于
令
不能把
预处理
下方模板在每次调用 C 时重置全局计数 nn,并由 init 覆盖当前质数幂范围内的 mem。数组容量必须大于最大的 1LL 作为中间量,若把模数类型扩展到 long long,则应相应改用 __int128。
点击展开代码
cpp
int phi(int x, int y) { return x / y * (y - 1); }
int nn, a[N], b[N];
int mod[N], mem[N];
int intchina() {
int i, prod = 1, s = 0;
for (i = 1; i <= nn; i++)
prod = 1LL * prod * a[i];
for (i = 1; i <= nn; i++) {
int t = qz(prod / a[i], phi(a[i], mod[i]) - 1, a[i]);
s = (s + 1LL * b[i] * (prod / a[i]) % prod * t) % prod;
}
return s;
}
int n, m, p;
void init(int p, int mod) {
int i;
mem[0] = 1;
for (i = 1; i <= mod; i++) {
mem[i] = mem[i - 1];
if (i % p)
mem[i] = 1LL * mem[i] * i % mod;
}
}
int l(int x, int p) {
int res = p, ans = 0;
while (res <= x) {
ans += x / res;
if (res > x / p)
break;
res *= p;
}
return ans;
}
int g(int x, int mod) {
int res = qz(mem[mod], x / mod, mod);
return 1LL * res * mem[x % mod] % mod;
}
int f(int x, int p, int mod) {
if (x <= 1)
return 1;
return 1LL * f(x / p, p, mod) * g(x, mod) % mod;
}
int C(int n, int m, int p) {
if (n < m)
return 0;
nn = 0;
for (int i = 2; i <= p / i; i++) {
int res = 1;
while (p % i == 0) {
res *= i;
p /= i;
}
if (res > 1) {
a[++nn] = res;
mod[nn] = i;
}
}
if (p > 1) {
a[++nn] = p;
mod[nn] = p;
}
for (int i = 1; i <= nn; i++) {
int base = l(n, mod[i]) - l(m, mod[i]) - l(n - m, mod[i]);
int aa = qz(mod[i], base, a[i]);
int res1, res2;
init(mod[i], a[i]);
res1 = f(n, mod[i], a[i]),
res2 = 1LL * f(m, mod[i], a[i]) * f(n - m, mod[i], a[i]) % a[i];
aa = 1LL * aa * res1 % a[i];
aa = 1LL * aa * qz(res2, (a[i] - a[i] / mod[i]) - 1, a[i]) % a[i];
b[i] = aa;
}
return intchina();
}BSGS
bsgs 用于求解指数同余方程,即:
其基本原理是任意一个
枚举
具体到原问题中,首先确定
当
预处理
所以可以发现,bsgs 建立在
有求逆元和 map 询问的存在,时间复杂度:
小优化:
考虑将
预处理 hashmap 的时间复杂度视作
省去了求逆元的时间复杂度。(但是上式仍建立在逆元存在的基础上)
或者实际上本质上是求
点击展开代码
cpp
struct BSGS {
unordered_map<int, int> mp;
int bsgs(int a, int b, int p) {
if (b == 1)
return 0;
b %= p;
int sqt = ceil(sqrt(p));
int A = b;
mp.clear();
for (int i = 0; i < sqt; i++) {
mp[A] = i;
A = 1LL * A * a % p;
}
int base = qz(a, sqt, p);
A = 1;
for (int i = 1; i <= sqt; i++) {
A = 1LL * A * base % p;
if (mp.count(A))
return i * sqt - mp[A];
}
return -1;
}
};扩展 BSGS
用于求解:
因为
若
此时,原式为:
采用 bsgs 求解后,
时间复杂度:
点击展开代码
cpp
struct ExBSGS {
int exbsgs(int a, int b, int c) {
int sqt, base, i, A = 1;
hashmap.clear();
for (int i = 0; i < 32; i++) {
if (A % c == b)
return i;
A = 1LL * A * a % c;
}
int d = 1, cnt = 0;
while (gcd(a, c) != 1) {
int g = gcd(a, c);
if (b % g != 0)
return -1;
b /= g;
c /= g;
d = 1LL * d * (a / g) % c;
cnt++;
}
sqt = ceil(sqrt(c));
A = b;
for (int i = 0; i < sqt; i++) {
hashmap[A] = i;
A = 1LL * A * a % c;
}
base = qz(a, sqt, c);
A = 1LL * d * base % c;
for (int i = 1; i <= sqt; i++) {
auto it = hashmap.find(A);
if (it != hashmap.end())
return i * sqt - it->second + cnt;
A = 1LL * A * base % c;
}
return -1;
}
int calc(int b, int n, int p) {
b %= p, n %= p;
if (n == 1 || p == 1)
return 0;
if (b == 0 && n != 0)
return -1;
return exbsgs(b, n, p);
}
};离散对数
对于模数
因此,只有满足
存在原根的正整数模数恰为
记作
所以求解底数是原根时的特殊指数同余方程,可以使用 bsgs 解决。
模数为质数的 N 次剩余
对于
当
一般意义下的 N 次剩余
对于合数模数,可以先分解为
特别地,
中国剩余定理
用于求解线性同余方程组
令
则原同余方程组的解为
可以证明,线性同余方程组在模意义下解唯一,所以上述解即为原方程组的唯一解。
时间复杂度:
注:若不保证模数为质数,仅保证两两互质,那么可以使用欧拉定理预处理欧拉函数求逆元。或直接使用 exgcd 求解逆元。
实现时还必须保证 __int128 检查中间结果;若最终模数超过当前 int 的范围则返回 -2。若把求逆步骤改成费马小定理
点击展开代码
cpp
struct CRT {
int intchina(vector<int> &r, vector<int> &mod, int n) {
int M = 1;
for (int i = 1; i <= n; i++) {
__int128 nxt = (__int128)M * mod[i];
if (nxt > numeric_limits<int>::max())
return -2;
M = (int)nxt;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (mod[i] == 1)
continue;
int now = M / mod[i];
int inv = qz(now % mod[i], phi[mod[i]] - 1, mod[i]);
ans = (ans + (__int128)r[i] * now % M * inv) % M;
}
return ans;
}
};garner 算法
若
我们可以用以下形式的式子(称作
garner 算法用来计算系数
令
把
代入第二个方程得出:
方程两边减
类似地,我们可以得到:
时间复杂度:
下方代码用 mod[j] 都是质数。若允许合数模数,应改用 exgcd 求逆。混合基数的累积乘积和最终答案同样可能溢出;模板返回 -2 表示结果已经超出当前 int 的可表示范围。
三模数 NTT 实现的任意模数 NTT 就是这个原理。
点击展开代码
cpp
int Garner(vector<int> &r, vector<int> &mod, int n) {
vector<int> x(n + 1);
vector<vector<int>> iv(n + 1, vector<int>(n + 1));
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
iv[i][j] = qz(mod[i], mod[j] - 2, mod[j]);
}
}
for (int i = 1; i <= n; i++) {
x[i] = r[i];
for (int j = 1; j < i; j++) {
x[i] = (__int128)iv[j][i] * (x[i] - x[j]) % mod[i];
if (x[i] < 0)
x[i] += mod[i];
}
}
int res = x[1];
int now = 1;
for (int i = 2; i <= n; i++) {
__int128 nxt = (__int128)now * mod[i - 1];
if (nxt > numeric_limits<int>::max())
return -2;
now = (int)nxt;
nxt = (__int128)res + (__int128)now * x[i];
if (nxt > numeric_limits<int>::max())
return -2;
res = (int)nxt;
}
return res;
}扩展中国剩余定理
用于求解线性同余方程组
对于两个同余方程
写成二元一次方程的形式:
使用 exgcd 求解即可。
注意会出现
将解出的
至此,成功将两个同余方程合并成了一个同余方程。
依次类推,合并
实际上此处 crt 和 excrt 讨论的都是
对于一般的同余方程
时间复杂度:
合并后的模数会逐步变成最小公倍数,也可能超过整数范围。下方模板使用 __int128 计算新的模数和余数;无解返回 -1,结果模数超出当前 int 范围则返回 -2。若连最终最小公倍数都超过 64 位,仅扩大中间乘法仍不够,需要使用大整数或改变输出约定。
点击展开代码
cpp
int eyyc(int a, int b, int c) {
int g = gcd(a, b);
if (c % g)
return -1;
int x, y;
exgcd(a, b, x, y);
b /= g;
x = ((__int128)x * (c / g) % b + b) % b;
return x;
}
struct ExCRT {
int intchina(vector<int> &r, vector<int> &mod, int n) {
int R = r[1], M = mod[1];
for (int i = 2; i <= n; i++) {
int rhs = ((__int128)R - r[i]) % mod[i];
int x = eyyc(M, mod[i], rhs); // b 取正值,保证求解得到的一定是非负值
if (x == -1)
return -1;
__int128 nxtM = (__int128)M / gcd(M, mod[i]) * mod[i];
if (nxtM > numeric_limits<int>::max())
return -2;
__int128 nxtR = (__int128)R - (__int128)x * M;
nxtR %= nxtM;
if (nxtR < 0)
nxtR += nxtM;
R = (int)nxtR;
M = (int)nxtM;
}
return R;
}
};同余最短路:
同余最短路用于求解模意义下从
例如给定正整数
若选中任一
即可以关于
在每个同余类中求出最小可达值 dist[r] 后,可以判断给定整数是否可表示,也可以求给定下界
再对所有可达剩余类取最小值即可。
在模意义下,多一个
转圈法求解同余最短路问题:
本质是:
所以对于不同的
转移
时间复杂度:
