阅读本文所需前置知识:欧几里得算法及其扩展,乘法逆元
# 中国剩余定理
中国古代的数学巨著《孙子算经》中提到一个 “物不知数” 问题,原文如下:” 有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?“。
用现代语言解释,既,一个整数除以三余二,除以五余三,除以七余二,求这个整数。《孙子算经》中首次提到了同余方程组问题,以及以上具体问题的解法,因此在中文数学文献中也会将中国剩余定理称为孙子定理。
现代数论中,中国剩余定理,是一个关于一元线性同余方程组的定理,说明了一元线性同余方程组有解的准则以及求解方法。
# 前置知识:一元线性同余方程
在数论中,线性同余方程是最基本的同余方程,“线性” 表示方程只有一个未知数,即形如:ax≡b(modm) 的方程。
# 如何求出未知数 x 呢?
- 根据同余的含义:(ax−b)modm=0 ,那么一定有整数
y
使得 my+b=ax 。 - 把 my 移动到右面,得到:ax−my=b 。
- 如果我们假设 y′=−y ,那么步骤二的方程可以写为:ax+my′=b 。
- 根据裴蜀定理,上面的方程有解的前提是
gcd(a,m)|b
,其中 |
表示整除。 - 用扩展欧几里德算法求出一组整数 x0,y0 ,使得ax0+my0=gcd(a,m) 。
- 现在有两组等式:1. ax+my′=b ;2. ax0+my0=gcd(a,m) 。其中
b
又一定是 gcd(a,m)
的倍数,那么我们只要将 等式2
两面同时扩大 b/gcd(a,m)
倍不就能得到 等式1
了嘛。 - 扩大后就得到了x=x0∗b/gcd(a,m) 。
- 此时b/gcd(a,m) 的系数是 1,那么我们只要把这个系数扩展到全部整数,就得到了 x 的通解:x0+kdn∣k∈Z ,其中
d=gcd(a,m)
。
# Code
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一组数据 a,b,m。
输入样例
2
2 3 6
4 3 5
输出样例
impossible
-3
| #include <iostream> |
| using namespace std; |
| typedef long long LL; |
| |
| int exgcd(int a, int m, int &x, int &y) { |
| if (!m) { |
| x = 1, y = 0; |
| return a; |
| } |
| int d = exgcd(m, a % m, y, x); |
| y -= a / m * x; |
| return d; |
| } |
| |
| int main() { |
| int n; |
| cin >> n; |
| while (n -- ) { |
| int a, b, m, x, y; |
| cin >> a >> b >> m; |
| int d = exgcd(a, m, x, y); |
| if (b % d) puts("impossible"); |
| |
| else cout << (LL)x * b / d % m << endl; |
| } |
| return 0; |
| } |
# 标准中国剩余定理的推导
在上面,我们知道了单个线性同余方程的求解方法,那么如果线性同余方程扩展为线性同余方程组呢?
对于问题∀i∈[1,n],x≡ai(modmi) ,若其中m1,m2,...,mn 满足两两互质,则可以使用标准的中国剩余定理求解,形如:
⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧x≡a1(modm1)x≡a2(modm2)⋮x≡an(modmn)
求解公式 x=∑i=1naiMiti
其中M=m1∗m2∗m3∗...∗mn ,Mi=M/mi (也就是除了mi 以外的n−1 个数的乘积) ,ti 为Mi 的逆元。即,tiMi≡1(modmi)。
# 公式证明
在上面公式的基础上左右两面同时乘以 ai ,得到: aitiMi≡ai(modmi) 。
又因为当j=i 时,每个Mj 中都被乘了一个mi (没明白的,再看一遍Mj 的定义),所以有 ajtjMj≡0(modmi)。
所以当 i=1
时,对于求解公式x=a1M1t1+a2M2t2+a3M3t3+...+akMktk 中的每一项除了a1M1t1=a1∗1=a1 之外,全部是akMktk=ak∗0 。所以有x=a1∗1+a2∗0+...+ak∗0=a1(modm1) ,那么当 i 取其他值时候同理。这样我们就证明了最终的线性同余方程组的求解公式,也就是中国剩余定理。
# 举个例子
(公式推导实在太抽象了。不如使用实际的例子推一遍来的清晰。)
以《孙子算经》里面的 “物不知数” 问题为例,其中的线性同余方程组是:
⎩⎪⎨⎪⎧x≡2(mod3)x≡3(mod5)x≡2(mod7)
- 首先把例子给的值对应到模型上,其中a1=2,a2=3,a3=2,m1=3,m2=5,m3=7 。
- 首先算出 M,M=3∗5∗7=105 ,再依次求出M1,M2,M3 ,M1=M/m1=35,M2=21,M3=15 , 算出Mi 的逆元ti ,其中因为 m 可能是非质数,所以这里求逆元要用扩展欧几里得算法,求出t1=2,t2=1,t3=1 。
- 代入到求解公式:x=2∗35∗2+3∗21∗1+2∗15∗1=233。
- 而
233
只是原线性同余方程组的一个解,那么他的通解为:x=233+k∗105,k∈Z。 - 通过通解,我们还可以进一步求出最小的非负整数解,即当
k=-2
时, x=23
。
# 扩展中国剩余定理的推导
注意在标准的中国剩余定理上,有一个重要的前提:所有的 m 均满足两两互质。
扩展中国剩余定理可以在不满足这个重要条件下,仍然求出 最小的非负整数x
。
# 公式推导
- 先只看前两个方程:
{x≡a1(modm1)x≡a2(modm2)
- 把模数运算转换为四则运算:
{x=k1∗m1+a1x=k2∗m2+a2
联立两个公式,得到:k1∗m1+a1=k2∗m2+a2 。
移项,得到:k1m1−k2m2=a2−a1 。
也就是:k1m1+k2(−m2)=a2−a1 。如此,我们就把公式转换成了扩展欧几里德算法的样式。
如标准中国剩余定理中的推导一样,使用扩展欧几里德找出一组解k1′,k2′ ,使得k1′∗m1+k2′∗(−m2)=gcd(m1,−m2) 。
此时的无解条件,就是gcd(m1,−m2) 无法整除a2−a1 。(参考前置知识线性同余方程的无解条件)
假设,gcd(m1,−m2)=d,(a2−a1)/d=y ,此时,显然只要把k1′,k2′ 同时扩大 y 倍,就能得到k1,k2 。
根据线性同余方程的通解,我们知道一个重要的性质,即:
{k1=k1′+k∗dm2k2=k2′+k∗dm1
当 k
为任意整数时,新的k1,k2 仍然满足公式k1m1+k2(−m2)=a2−a1 。
此处的证明:
- 将新的 k1,k2 带入到公式中,得到:(k1′+k∗dm2)∗m1+(k2′+k∗dm1)∗(−m2)=a2−a1
- 将括号展开:k1′∗m1+k2′∗(−m2)+k∗dm2∗m1−k∗dm1∗m2=a2−a1
- 消去同加同减,得到:k1′∗m1+k2′∗(−m2)=a2−a1
- 该公式本质与原公式是一样的,因此步骤 9 的性质成立。
把上面得到的新k1,k2 带入到原始方程组:
x=k1m1+a1=(k1′+k∗dm2)∗m1+a1=m1∗k1′+a1+k∗dm2m1
上面公式的前半部分m1∗k1′+a1 ,全部是已知的,也就是这部分可以看成一个新的整数,我们记作x0;后面部分的dm2m1 则是m2m1 的最小公倍数,也是一个确定的数,我们记作m0 。
那么,上述公式可以写成:m1∗k1′+a1+k∗dm2m1=x0+k∗m0 。
所以我们得到了前两个方程的通解:k∗m0+a0 。
可以看到该通解与原方程组中的方程非常相似。因此,我们使用这种方式两两合并,最后就可以合并出整个方程组的通解。
# Code
输入格式
第 1 行包含整数 n。
第 2…n+1 行:每 i+1 行包含两个整数 ai 和 mi,数之间用空格隔开。
输出格式
输出最小非负整数 x,如果 x 不存在,则输出 −1。
如果存在 x,则数据保证 x 一定在 64 位整数范围内。
输入样例
2
8 7
11 9
输出样例
31
| #include <iostream> |
| 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; |
| } |
| int 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; |
| bool has_answer = true; |
| for (int i = 0; i < n - 1; i ++ ) { |
| LL m2, a2; |
| cin >> m2 >> a2; |
| LL k1, k2; |
| LL d = exgcd(m1, m2, k1, k2); |
| |
| if ((a2 - a1) % d) { |
| has_answer = false; |
| break; |
| } |
| k1 *= (a2 - a1) / d; |
| LL t = m2 / d; |
| |
| k1 = (k1 % t + t) % t; |
| a1 = m1 * k1 + a1; |
| m1 = abs(m1 / d * m2); |
| } |
| if (has_answer) { |
| |
| cout << (a1 % m1 + m1) % m1 << endl; |
| } |
| else puts("-1"); |
| return 0; |
| } |
END