
同余方程学习笔记
解各种同余方程是同余问题的一个重要部分,本文介绍各种同余方程的求解方法。
Table of Contents
二元线性不定方程
形式
不定方程的范围很广泛,只要没有确定的解的方程都可以叫不定方程。本文主要讨论数论中的不定方程。
形如上面这种形式,满足各项的系数和解均为整数的方程就是数论中的不定方程。
求解
Theorem 1:,一定存在 ,使得 。
这个定理又叫做裴蜀定理,根据这个定理,可以得出不定方程 有解的充要条件就是 。记 ,则我们可以先求出 的解,再令 ,就得出原方程的一组解。
求 的过程可以用扩展欧几里得算法,即 exgcd 解决。
Theorem 2:若 ,且 是方程 的一组解,则该方程的通解为 ,其中 。
换句话说, 这两个解是具有周期性的。根据这个定理,我们知道一定有一个 ,而对于方程 ,则一定有一个 ,由此可以求一个不定方程的一个元的最小非负整数解,具体地,假设知道了一个解 ,则最小非负整数 ,其中 。
总结一下,求解二元线性不定方程 的流程如下:
- 求出 的一组解 ,并根据 判断方程有无解
- 令 , 为 的一组解
- 若需要求 的最小非负整数解,则将 作为最终的答案,其中
线性同余方程
形式
形如这样的方程,就被称线性同余方程。
求解
任意的线性同余方程都可以与一个二元线性不定方程互相转化,比如上面的方程就与二元线性不定方程有如下关系:
所以可以把线性同余方程转化为一个二元线性不定方程,求出 即可,包括有无解的判定条件都是一样的。
代码
template <typename T> T exgcd(T a, T b, T & x, T & y) {
if (b == 0) {
x = 1;
y = 0;
return a;
} else {
T d = exgcd(b, a % b, x, y);
T t = x;
x = y;
y = t - a / b * y;
return d;
}
}
template <typename T> optional<T> solveEquation(T a, T c, T p) {
a = (a % p + p) % p;
c = (c % p + p) % p;
T x, y;
T d = exgcd(a, p, x, y);
if (c % d != 0)
return nullopt;
x = (1ll * (c / d) * x % (p / d) + (p / d)) % (p / d);
return x;
}
中国剩余定理
形式
形如上面这样的线性同余方程组且满足每个方程的模数两两互质,就可以用中国剩余定理解决。中国剩余定理也叫做 CRT (Chinese Remainder Theorem)。
求解
- 令 , 不对任何数取模
- 对于 ,令 , 为 对 的逆元,, 不能对 取模
- 令 ,则 为模 意义下的唯一解
算法正确性证明
对于 ,,故 ,所以在模 情况下,。
因为 为 对 的逆元,故在模 的同余式中 ,,即 ,满足第 个方程。
解在模 意义下的唯一性证明
设 不相等,,但都满足方程组,则可以有下面的方程组:
则 ,所以 ,即 ,因为 ,故 ,推出 ,与假设矛盾。
代码
int exgcd(int a, int b, int & x, int & y) {
if (b == 0) {
x = 1;
y = 0;
return a;
} else {
int d = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - (a / b) * y;
return d;
}
}
int solveEquation(int a, int c, int p) {
int x, y;
exgcd(a, p, x, y);
x = (1ll * x * c % p + p) % p;
return x;
}
inline int inv(int x, int p) {
return solveEquation(x, 1, p);
}
int chineseRemainderTheorem(int n, int a[], int m[]) {
int mod = 1, ans = 0;
for (int i = 1; i <= n; ++i)
mod *= m[i];
for (int i = 1; i <= n; ++i) {
int modi = mod / m[i];
ans = (ans + 1ll * a[i] * modi * inv(modi)) % mod; // 用 exgcd 求逆元
}
return ans;
}
扩展中国剩余定理
形式
相比普通中国剩余定理求解的方程组,少了模数互质的限制。
求解
考虑合并方程。设当前要合并前 个方程,显然可以转化为合并前 个方程与第 个方程。
设 为合并前 个方程的解,。构造 ,我们要使 能够满足前 个方程,就要满足以下条件:
- ,
最简单的办法就是令 ,并求一个合适的 ,也就是要求出以下方程的解 :
用 exgcd 解决,若该线性同余方程无解,则方程组无解,否则求出前 个方程的解 ,进行下一次合并。
假设各个 同阶,时间复杂度 。
代码
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
int exgcd(int a, int b, int & x, int & y) {
if (b == 0) {
x = 1;
y = 0;
return a;
} else {
int d = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - (a / b) * y;
return d;
}
}
optional<int> solveEquation(int a, int c, int p) {
a = (a % p + p) % p;
c = (c % p + p) % p;
int x, y, d = exgcd(a, p, x, y);
if (c % d)
return nullopt;
x *= (c / d);
int m = p / d;
x = (x % m + m) % m;
return x;
}
optional<int> exChineseRemainderTheorem(int n, int a[], int c[], int m[]) {
int x = 0, d = 1;
for (int i = 1; i <= n; ++i) {
auto oy = solveEquation(a[i] * d, c[i] - a[i] * x, m[i]);
if (!oy.has_value())
return nullopt;
int y = oy.value();
int g = gcd(d, m[i]);
d = d / g * m[i];
x = (x + 1ll * (d / m[i] * g) * y) % d;
}
return x;
}
BSGS
离散对数
与离散对数相对的是我们熟悉的连续对数,也就是最经常接触的对数。而离散对数是在同余中的一个概念。
若在模 意义下, 满足以下等式:
则 为以 为底的 的离散对数。注意离散对数不同于连续对数,一个数的离散对数有很多个,并不是唯一的。
如果 ,则我们可以使用 BSGS 算法求解一个数的最小的离散对数。
求解
因为 ,则 对 的逆元一定存在。所以我们可以把 写成 并直接将 提取出来,即:
考虑暴力的思路,可以枚举两边的 ,如果有同余的两个 ,就更新答案。
考虑到枚举会计算许多重复的东西,可以用某种数据结构维护映射表,存下每个 ,再枚举左边找相应的同余的项。我们先假设 ,则当 时,时间复杂度最优,为 或 ,具体为哪种复杂度,取决于是用哈希表还是平衡树实现。
可解性证明
至于为什么一定有 ,使用抽屉原理证明:
对于任意的 , 最多只有 种取值,而在 时,一共有 种指数,则根据抽屉原理得出必定存在一对 满足 。设 ,则 ,因为已经保证 ,则逆元一定存在,所以可以规定 ,所以指数具有周期性,一定可以得到 。
代码
int power(int x, int y, int m) {
int res = 1;
for (; y; y /= 2) {
if (y % 2)
res = 1ll * res * x % m;
x = 1ll * x * x % m;
}
return res;
}
optional<int> bsgs(int a, int b, int m) {
a %= m;
b %= m;
unordered_map<int, int> buc;
int s = ceil(sqrt(m)), prod = b, base = power(a, s, m);
buc[b] = 0;
for (int i = 0; i < s; ++i) {
prod = 1ll * prod * a % m;
buc[prod] = i;
}
prod = 1;
for (int i = 0; i <= s; ++i) {
auto it = buc.find(prod);
if (it != buc.end() && i * s - it->second >= 0)
return i * s - it->second;
prod = 1ll * prod * base % m;
}
return nullopt;
}
扩展 BSGS
形式
其中 不一定互质。这种情况下,逆元不一定存在,也就不可以把指数拆成两个部分来优化。
考虑先把式子化为 的形式。方法是不断提取出 与 互质的部分,任何整个式子同除以这个数。
这个式子是同余式的基本性质。所以按照这样的方法提取:
假设现在 与 互质,就可以用 BSGS 求出最后一个方程的解 ,再加上 就是 了。
需要注意两点:
- 若在提取过程中,出现 ,则方程无法再继续提取,方程无解
- 有可能答案在 内,只要 枚举检验即可。
int gcd(int a, int b) {
int t;
while (b) {
t = a;
a = b;
b = a % b;
}
return a;
}
int power(int x, int y, int m) {
int res = 1;
for (; y; y /= 2) {
if (y % 2)
res = 1ll * res * x % m;
x = 1ll * x * x % m;
}
return res;
}
int exgcd(int a, int b, int & x, int & y) {
if (b == 0) {
x = 1;
y = 0;
return a;
} else {
int d = exgcd(b, a % b, x, y);
int t = x;
x = y;
y = t - a / b * y;
return d;
}
}
int inverse(int a, int p) {
int x, y;
exgcd(a, p, x, y);
x = (x % p + p) % p;
return x;
}
optional<int> bsgs(int a, int b, int m) {
a %= m;
b %= m;
unordered_map<int, int> buc;
int s = ceil(sqrt(m)), prod = b, base = power(a, s, m);
buc[b] = 0;
for (int i = 0; i < s; ++i) {
prod = 1ll * prod * a % m;
buc[prod] = i;
}
prod = 1;
for (int i = 0; i <= s; ++i) {
auto it = buc.find(prod);
if (it != buc.end() && i * s - it->second >= 0)
return i * s - it->second;
prod = 1ll * prod * base % m;
}
return nullopt;
}
optional<int> exBsgs(int a, int b, int m) {
a = (a % m + m) % m;
b = (b % m + m) % m;
if (m == 1 || b == 1)
return 0;
int prod = 1, d, k = 0;
while (true) {
d = gcd(a, m);
if (d == 1)
break;
if (b % d)
return nullopt;
b /= d;
m /= d;
++k;
prod = prod * (a / d) % m;
if (prod == b)
return k;
}
auto res = bsgs(a, 1ll * b * inverse(prod, m) % p, m);
if (!res.has_value())
return nullopt;
return res.value() + k;
}
阶与原根
阶
对于一对互质的整数 ,若 满足 且 且不存在任何小于 的正整数满足该同余式,则称 为 对 的阶,记作 。
Theorem 1:。
若 ,则欧拉定理一定成立,即 ,显然 。
Theorem 2:若 ,则 。
假设满足 但 ,则 ,其中 ,所以:
根据阶的定义,,所以矛盾,故 。
原根
若 满足 ,则 为 的原根。
Theorem 1: 为 的原根的充要条件为 且对于 的每一个质因数,。
根据这个定理,可以轻松判断一个数是否是另一个数的原根。
int gcd(int a, int b) {
int t;
while (b) {
t = a;
a = b;
b = t % b;
}
return a;
}
int phi(int x) {
int res = x;
for (int i = 2; i * i <= x; ++i) {
if (x % i)
continue;
res = res / i * (i - 1);
while (x % i)
x /= i;
}
if (x > 1)
res = res / x * (x - 1);
return res;
}
int power(int x, int y, int m) {
int res = 1;
for (; y; y /= 2) {
if (y % 2)
res = 1ll * res * x % m;
x = 1ll * x * x % m;
}
return res;
}
bool isPrimitiveRoot(int a, int m) {
if (gcd(a, m) != 1)
return false;
int pm = phi(m), tmp = pm;
a %= m;
for (int i = 2; i * i <= tmp; ++i) {
if (tmp % i)
continue;
if (power(a, pm / i, m) == 1)
return false;
while (tmp % i == 0)
tmp /= i;
}
if (tmp > 1)
if (power(a, pm / tmp, m) == 1)
return false;
return true;
}
Theorem 2:一个数 有原根,当且仅当 为 ,其中 , 为奇质数。
Theorem 3:若一个数 有原根,则其原根个数为 。
Theorem 4:若一个数 有原根,则其最小原根的数量级为 。
根据这个性质,只需要暴力枚举每个数,求一个数的最小原根的时间复杂度是可以接受的。
int gcd(int a, int b) {
int t;
while (b) {
t = a;
a = b;
b = t % b;
}
return a;
}
int phi(int x) {
int res = x;
for (int i = 2; i * i <= x; ++i) {
if (x % i)
continue;
res = res / i * (i - 1);
while (x % i)
x /= i;
}
if (x > 1)
res = res / x * (x - 1);
return res;
}
int power(int x, int y, int m) {
int res = 1;
for (; y; y /= 2) {
if (y % 2)
res = 1ll * res * x % m;
x = 1ll * x * x % m;
}
return res;
}
int primitiveRoot(int m) {
int pm = phi(m), tmp = pm;
vector<int> factor;
for (int i = 2; i * i <= tmp; ++i) {
if (tmp % i)
continue;
factor.push_back(i);
while (tmp % i == 0)
tmp /= i;
}
if (tmp > 1)
factor.push_back(tmp);
for (int i = 2; i <= m; ++i) {
if (gcd(i, m) != 1)
continue;
bool found = true;
for (int p : factor) {
if (power(i, pm / p, m) == 1) {
found = false;
break;
}
}
if (found)
return i;
}
return -1;
}
Theorem 5:若 为 的原根,则 两两模 不同余。
Theorem 6:若 为 的原根,则对于任意的 ,一定有一个 与其一一对应,满足 且 , 被称为 的指标,记作 。
比如 ,其最小原根 ,则把 与 写成表格:
这个定理可以用于把底数转换为原根的幂,可以利用这个性质解模数为质数的 次剩余问题。
N 次剩余
形式
其中 为奇质数,求 。由于 不是奇质数的方法较复杂,这里不考虑。
求解
设 为其原根,则可以利用原根和指标的性质将 转化为 ,即:
这样就可以用 BSGS 求出 ,再用快速幂求得 即可。
也可以再将 化为 ,然后方程化为:
于是就可以解出线性同余方程的解 :
代码就不写了,上面组合一下就可以了。