【算法学习笔记】37:扩展中国剩余定理(EXCRT)求解任意线性同余方程组
中国剩余定理仅适用于模数 m 1 . . . m n m_1...m_n m1...mn两两互质的情况,如果没有这个限制条件则不能使用。
下面进行一般情况的解法推导,注意虽然叫扩展中国剩余定理(EXCRT),实际上只是扩宽了中国剩余定理(CRT)的求解限制,解法和原理上和CRT完全没啥关系。
1 推导过程
对于一般的线性同余方程组:
x
m
o
d
m
1
≡
a
1
x
m
o
d
m
2
≡
a
2
.
.
.
x
m
o
d
m
n
≡
a
n
x~mod~m_1 \equiv a_1 \\ x~mod~m_2 \equiv a_2 \\ ... \\ x~mod~m_n \equiv a_n
x mod m1≡a1x mod m2≡a2...x mod mn≡an
1.1 对于任意两个方程求得一组解 k 1 k_1 k1、 k 2 k_2 k2
先考虑其中两个方程组成的方程组:
x
m
o
d
m
1
≡
a
1
x
m
o
d
m
2
≡
a
2
x~mod~m_1 \equiv a_1 \\ x~mod~m_2 \equiv a_2
x mod m1≡a1x mod m2≡a2
说明存在
k
1
k_1
k1、
k
2
k_2
k2分别是
x
x
x除以
m
1
m_1
m1、
m
2
m_2
m2的解,并留下余数是
a
1
a_1
a1和
a
2
a_2
a2,也即:
x
=
k
1
⋅
m
1
+
a
1
x
=
k
2
⋅
m
2
+
a
2
x = k_1 \cdot m_1 + a_1 \\ x = k_2 \cdot m_2 + a_2
x=k1⋅m1+a1x=k2⋅m2+a2
从而可以用
x
x
x把两式关联起来:
k
1
⋅
m
1
+
a
1
=
k
2
⋅
m
2
+
a
2
k_1 \cdot m_1 + a_1 = k_2 \cdot m_2 + a_2
k1⋅m1+a1=k2⋅m2+a2
整理即得一个不定方程:
k
1
⋅
m
1
−
k
2
⋅
m
2
=
a
2
−
a
1
k_1 \cdot m_1 - k_2 \cdot m_2 = a_2 - a_1
k1⋅m1−k2⋅m2=a2−a1
其中 m 1 m_1 m1、 m 2 m_2 m2、 a 2 a_2 a2、 a 1 a_1 a1都是给定的,只要求出 k 1 k_1 k1和 k 2 k_2 k2就能算出 x x x了,只要用扩展欧几里得算法就能计算 k 1 k_1 k1和 − k 2 -k_2 −k2,仅当 a 2 − a 1 a_2-a_1 a2−a1能被 m 1 m_1 m1和 m 2 m_2 m2的最大公约数整除时候有解,即 g c d ( m 1 , m 2 ) ∣ a 2 − a 1 gcd(m_1, m_2)~|~a_2 - a_1 gcd(m1,m2) ∣ a2−a1。
1.2 从 k 1 k_1 k1、 k 2 k_2 k2得到 x x x的所有解的表示形式
从前面的推导可以得出:
x
=
k
1
⋅
m
1
+
a
1
x
=
k
2
⋅
m
2
+
a
2
x = k_1 \cdot m_1 + a_1 \\ x = k_2 \cdot m_2 + a_2
x=k1⋅m1+a1x=k2⋅m2+a2
只要得到了一组 k 1 k_1 k1、 k 2 k_2 k2,已经满足了 k 1 ⋅ m 1 − k 2 ⋅ m 2 = a 2 − a 1 k_1 \cdot m_1 - k_2 \cdot m_2 = a_2 - a_1 k1⋅m1−k2⋅m2=a2−a1,对于最大公约数 d = g c d ( m 1 , m 2 ) d = gcd(m_1, m_2) d=gcd(m1,m2),只要找任何一个倍数 k k k,让 k 1 k_1 k1加上 k ⋅ m 2 d k \cdot \frac{m_2}{d} k⋅dm2,让 k 2 k_2 k2加上 k ⋅ m 1 d k \cdot \frac{m_1}{d} k⋅dm1,该等式还是成立的。
所以引入整数
k
k
k来表示
x
x
x的所有解,应该形如(只看第一个式子的话):
x
=
(
k
1
+
k
m
2
d
)
m
1
+
a
1
x = (k_1 + k \frac{m_2}{d})m_1 + a_1
x=(k1+kdm2)m1+a1
整理一下,得到:
x
=
m
1
k
1
+
a
1
+
k
m
1
m
2
d
x = m_1k_1 + a_1 + k\frac{m_1m_2}{d}
x=m1k1+a1+kdm1m2
其中
d
=
g
c
d
(
m
1
,
m
2
)
d = gcd(m_1, m_2)
d=gcd(m1,m2),所以
m
1
m
2
d
\frac{m_1m_2}{d}
dm1m2也即
l
c
m
(
m
1
,
m
2
)
lcm(m_1, m_2)
lcm(m1,m2),因此:
x
=
m
1
k
1
+
a
1
+
k
⋅
l
c
m
(
m
1
,
m
2
)
x = m_1k_1 + a_1 + k \cdot lcm(m_1, m_2)
x=m1k1+a1+k⋅lcm(m1,m2)
1.3 任意两个不定方程的合并
其中
m
1
k
1
+
a
1
m_1k_1 + a_1
m1k1+a1这一项在求出
k
1
k_1
k1之后就是固定的了,记为
A
A
A,后面的
l
c
m
(
m
1
,
m
2
)
lcm(m_1, m_2)
lcm(m1,m2)记为
M
M
M,则
x
x
x的所有解的形式就可以表示为:
x
=
k
⋅
M
+
A
x = k \cdot M + A
x=k⋅M+A
这个方程不仅和前面的不定方程形式一样,而且其中
M
M
M和
A
A
A都是确定的,
k
k
k是变化的。所以总是可以通过这样的方式把两个不定方程合并成一个,也即对于前两个方程
x
=
k
1
⋅
m
1
+
a
1
x
=
k
2
⋅
m
2
+
a
2
x = k_1 \cdot m_1 + a_1 \\ x = k_2 \cdot m_2 + a_2
x=k1⋅m1+a1x=k2⋅m2+a2
可以合并为
x
=
k
M
+
A
x = k M + A
x=kM+A,其中
M
=
l
c
m
(
m
1
,
m
2
)
M = lcm(m_1, m_2)
M=lcm(m1,m2),
A
=
m
1
k
1
+
a
1
A = m_1k_1 + a_1
A=m1k1+a1,然后可以拿着新方程和第三个方程继续合并,即合并:
x
=
k
⋅
M
+
A
x
=
k
3
⋅
m
3
+
a
3
x = k \cdot M + A \\ x = k_3 \cdot m_3 + a_3
x=k⋅M+Ax=k3⋅m3+a3
一共有 n n n个方程,只要这样合并 n − 1 n-1 n−1次就只剩一个方程 x = k ⋅ M + A x = k \cdot M + A x=k⋅M+A了,这时要找 x x x的最小正整数取值就很简单了,一定是 A A A模 M M M的正的余数。
2 例题:AcWing 204. 表达整数的奇怪方式
注意这题里 a a a和 m m m的含义和前面的推导是相反的。
这题里限制了方程的数量 n n n是不超过25个的,因为每次合并两个方程,得到的新方程的 M M M都是前两个方程里 m i m_i mi和 m j m_j mj的最小公倍数,所以最后的 M M M就是 m 1 . . . m n m_1...m_n m1...mn的最小公倍数,如果方程太多了这个最小公倍数就太大了,可能超出整数范围,所以数据里做了这样的限制。
为了防止得到的新的方程里 k k k越来越大爆掉,每次都取一个最小的正整数 k k k。
小技巧:要取任何一个整数p对于模q的数学模(最小正整数),只要(p % q + q) % q即可
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
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;
}
int main() {
int n; cin >> n;
// 读取第一组方程
LL m1, a1; cin >> m1 >> a1;
for (int i = 1; i < n; i ++ ) {
// 每次合并完读取下一组方程
LL m2, a2; cin >> m2 >> a2;
// 尝试合并两个方程
// x = k1 m1 + a1
// x = k2 m2 + a2
// k1 m1 - k2 m2 = a2 - a1
// 用exgcd计算k1和-k2(丢弃)
LL k1, k2;
LL d = exgcd(m1, m2, k1, k2);
// 如果有解,d必须是a2 - a1的倍数
if ((a2 - a1) % d) {
puts("-1");
return 0;
}
// 把d变成a2 - a1时候,k1也跟着倍增
k1 *= (a2 - a1) / d;
// 由于k1可以是任意的k1 + k * (m2 / d)
// 这里为了防止太大,每次把k1变成满足条件的最小正值
LL t = m2 / d;
k1 = (k1 % t + t) % t;
// 接下来有了k1,合并好的新方程是
// x = k1 M + A,其中
// A = m1 k1 + a1,M = lca(m2, m1)
// 这里M就是下一轮的m1,A就是下一轮的a1
a1 = m1 * k1 + a1;
m1 = m1 * m2 / d;
}
// 最后的解是上一轮的A = m1 k1 + a1,放在了a1里
// 为了调整k1让A是最小正整数,即是对m1取模时的最小正数
cout << (a1 % m1 + m1) % m1 << endl;
return 0;
}