数据结构与算法学习笔记----中国剩余定理
数据结构与算法学习笔记----中国剩余定理
@@ author: 明月清了个风
@@ first publish time: 2025.1.17ps⭐️一道中国剩余定理的扩展题,在做这道题之前了解之前的逆元和扩展欧几里得算法能够更好的帮助理解(因为中间涉及求逆的运算)
Acwing 204. 表达整数的奇怪方式
[原题链接](204. 表达整数的奇怪方式 - AcWing题库)
给定 2 n 2n 2n个整数 a 1 , a 2 , ⋯ , a n a_1,a_2,\cdots,a_n a1,a2,⋯,an和 m 1 , m 2 , ⋯ , m n m_1,m_2,\cdots,m_n m1,m2,⋯,mn,求一个最小的非负整数 x x x,满足 ∀ i ∈ [ 1 , n ] , x ≡ m i ( m o d a i ) \forall i \in[1,n], x \equiv m_i \pmod{a_i} ∀i∈[1,n],x≡mi(modai)。
输入格式
第一行包含整数 n n n,
第 2 , ⋯ , n + 1 2,\cdots,n+1 2,⋯,n+1行:每 i + 1 i+1 i+1行包含两个整数 a i a_i ai和 m i m_i mi,数之间用空格隔开。
输出格式
输出最小非负整数 x x x,如果 x x x不存在,则输出 − 1 -1 −1。
数据范围
1 ≤ a i ≤ 2 3 1 − 1 1 \le a_i \le 2^31 - 1 1≤ai≤231−1,
0 ≤ m i ≤ a i 0 \le m_i \le a_i 0≤mi≤ai,
1 ≤ n ≤ 25 1 \le n \le 25 1≤n≤25
所有 m i m_i mi的最小公倍数在 64 64 64位有符号整数范围内。
思路
中国剩余定理其实是一个公式,我们先给出这个定理:
中国剩余定理,又称孙子定理,是中国古代求解一次同余式组的方法,也是数论中的一个重要定理。它最早记载于中国南北朝时期(公元5世纪)的数学著作《孙子算经》卷下第二十六题,题为“物不知数”问题,原文如下:
有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?
用现代数学语言描述,即求解满足以下同余方程组的整数 x x x:
{ x ≡ 2 ( m o d 3 ) x ≡ 3 ( m o d 5 ) x ≡ 2 ( m o d 7 ) \begin{cases} x \equiv 2 \pmod{3} \\ x \equiv 3 \pmod{5} \\ x \equiv 2 \pmod{7} \end{cases} ⎩ ⎨ ⎧x≡2(mod3)x≡3(mod5)x≡2(mod7)
定理内容
设 m 1 , m 2 , … , m k m_1, m_2, \ldots, m_k m1,m2,…,mk 是 k k k 个两两互质的正整数,对于任意的整数 a 1 , a 2 , … , a k a_1, a_2, \ldots, a_k a1,a2,…,ak,同余式组
{ x ≡ a 1 ( m o d m 1 ) x ≡ a 2 ( m o d m 2 ) ⋮ x ≡ a k ( m o d m k ) (1) \begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \\ \vdots \\ x \equiv a_k \pmod{m_k} \tag{1} \end{cases} ⎩ ⎨ ⎧x≡a1(modm1)x≡a2(modm2)⋮x≡ak(modmk)(1)
有唯一解,解的形式为:
x ≡ M 1 ′ M 1 a 1 + M 2 ′ M 2 a 2 + … + M k ′ M k a k ( m o d M ) (2) x \equiv M_1' M_1 a_1 + M_2' M_2 a_2 + \ldots + M_k' M_k a_k \pmod{M} \tag{2} x≡M1′M1a1+M2′M2a2+…+Mk′Mkak(modM)(2)
其中, M = m 1 × m 2 × … × m k M = m_1 \times m_2 \times \ldots \times m_k M=m1×m2×…×mk, M i = M m i M_i = \frac{M}{m_i} Mi=miM, M i ′ M i ≡ 1 ( m o d m i ) M_i' M_i \equiv 1 \pmod{m_i} Mi′Mi≡1(modmi)(即 M i ′ M_i' Mi′ 是 M i M_i Mi 关于模 m i m_i mi 的逆元)。
定理中有一个很强的限制条件就是这些 m i m_i mi需要两两互质,但是我们的题干中并没有这个条件,因此需要进行一些推导
我们先从将题目简化看是否能够理出思路,假设有以下方程组需要求解
{
x
m
o
d
a
1
=
m
1
x
m
o
d
a
2
=
m
2
(3)
\begin{cases} x \mod a_1 = m_1 \\ x \mod a_2 = m_2 \\ \tag{3} \end{cases}
{xmoda1=m1xmoda2=m2(3)
那么就有下面的等式:
{
x
=
k
1
⋅
a
1
+
m
1
x
=
k
2
⋅
a
2
+
m
2
(4)
\begin{cases} x = k_1 \cdot a_1 + m_1 \\ x = k_2 \cdot a_2 + m_2 \\ \tag{4} \end{cases}
{x=k1⋅a1+m1x=k2⋅a2+m2(4)
利用式(5)中的等式就可以得到下面的式子:
k
1
⋅
a
1
+
m
1
=
k
2
⋅
a
2
+
m
2
(5)
k_1 \cdot a_1 + m_1 = k2 \cdot a_2 + m_2 \tag{5}
k1⋅a1+m1=k2⋅a2+m2(5)
移项可得:
k
1
⋅
a
1
−
k
2
⋅
a
2
=
m
2
−
m
1
(6)
k_1 \cdot a_1 - k_2 \cdot a_2 = m_2 - m_1 \tag{6}
k1⋅a1−k2⋅a2=m2−m1(6)
这里就是我们之前用扩展欧几里得算法求解的方程了,上式有解需要满足
m
2
−
m
1
m_2 - m_1
m2−m1能够整除
gcd
(
a
1
,
a
2
)
\gcd(a_1,a_2)
gcd(a1,a2),求出
k
1
,
k
2
k_1,k_2
k1,k2后即可得
x
x
x的值。
那么这里式(6)就可以看成是一个一次不定方程,我们假设其有解,即上述有解条件成立,那么就可以使用扩展欧几里得算法找到一组特解
(
k
1
′
,
k
2
′
)
(k_1',k_2')
(k1′,k2′),其通解形式就可以表示为
{
k
1
=
k
1
′
+
k
a
2
g
c
d
(
a
1
,
a
2
)
k
2
=
k
2
′
+
k
a
1
g
c
d
(
a
1
,
a
2
)
(7)
\begin{cases} k_1 = k_1' + k\frac{a_2}{gcd(a_1,a_2)} \\ k_2 = k_2' + k\frac{a_1}{gcd(a_1,a_2)} \\ \tag{7} \end{cases}
{k1=k1′+kgcd(a1,a2)a2k2=k2′+kgcd(a1,a2)a1(7)
那么再将上述结果带入式(5)即可得到
x
x
x的所有解的形式,这里将
gcd
(
a
1
,
a
2
)
\gcd(a_1,a_2)
gcd(a1,a2)记为
d
d
d
x
=
k
1
⋅
a
1
+
m
1
=
(
k
1
′
+
k
a
2
d
)
a
1
+
m
1
=
a
1
k
1
+
m
1
+
k
a
1
a
2
d
=
a
1
k
1
+
m
1
+
k
⋅
[
a
1
,
a
2
]
\begin{equation} \begin{aligned} x & = k_1 \cdot a_1 + m_1 \\ & = (k_1' + k\frac{a_2}{d}) a_1+m_1 \\ & = a_1k_1 + m_1 + k \frac{a_1 a_2}{d} \\ & = a_1k_1 + m_1 + k \cdot [a_1,a_2] \\ \tag{8} \end{aligned} \end{equation}
x=k1⋅a1+m1=(k1′+kda2)a1+m1=a1k1+m1+kda1a2=a1k1+m1+k⋅[a1,a2](8)
这里将
a
1
k
1
+
m
1
a_1k_1 + m_1
a1k1+m1记为新
x
0
x_0
x0,将
k
⋅
[
a
1
,
a
2
]
k \cdot [a_1,a_2]
k⋅[a1,a2]记为新的
a
a
a,则有
x
=
x
0
+
k
a
(9)
x = x_0 + ka \tag{9}
x=x0+ka(9)
也就是说我们可以将式(5)的两个不定等式变为式(10),每两个式(4)所示的方程可以缩减一次,那么
n
−
1
n-1
n−1次之后就可以只有一个方程了,而上面这个方程其实就是求一个最小正整数解
x
x
x满足,到这就可以开始写代码啦,代码中也给出了一些注释
x
m
o
d
a
≡
x
0
(10)
x \bmod a \equiv x_0 \tag{10}
xmoda≡x0(10)
代码
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL; // 需要long long,因为我们会求最多25个数的最小公倍数
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 a1, m1;
cin >> a1 >> m1;
bool flag = true;
for(int i = 0; i < n - 1; i ++) // 按照我们推导的过程,每两个方程进行一次合并
{
LL a2, m2;
cin >> a2 >> m2;
LL k1, k2;
LL d = exgcd(a1, a2, k1, k2);
if((m2 - m1) % d) // 扩展欧几里得算法有解的条件,找一下上文中高亮部分
{
flag = false;
break;
}
k1 *= (m2 - m1) / d; // 这里还是扩展欧几里得的内容,我们算的等式右边是(m2-m1)的最大公约数,系数要乘回去
LL t = a2 / d;
k1 = (k1 % t + t) % t; // 这里的操作是取一个最小的k,防止爆int
m1 = a1 * k1 + m1; // 这里就是上面的x0
a1 = abs(a1 / d * a2); // 这里就是上面的[a1,a2]
}
if(flag)
{
cout << (m1 % a1 + a1) % a1 <<endl; // 这里的操作和上面对k1的是一样的,求一个最小正整数解,应该不难理解
}
else puts("-1");
return 0;
}