算法学习笔记(38)——中国剩余定理
中国剩余定理
设 \(m_1, m_2,\cdots, m_n\) 是两两互质的整数,\(m = \prod_{i=1}^{n} m_i\) ,\(M_i = m / m_i\)(除了 \(m_i\) 之外其他所有 \(m\) 的乘积),\(t_i\) 是线性同余方程 \(M_it_i = 1 \pmod {m_i}\) 的一个解,即 \(t_i\) 是 \(M_i\) 模 \(m_i\) 的乘法逆元。对于任意的 \(n\) 个整数 \(a_1, a_2, \cdots, a_n\) ,方程组
有整数解,解为 \(x = \sum_{i=1}^{n} a_i M_i t_i\) 。
证明:
因为 \(M_i = m / m_i\) 是除 \(m_i\) 之外所有模数的倍数,所以 \(\forall k \neq i, a_iM_it_i \equiv 0 \pmod {m_k}\)。
又因为 \(a_iM_it_i \equiv a_i \pmod {m_i}\) ,所以代入 \(x = \sum_{i=1}^{n} a_iM_it_i\),原方程组成立。
证毕
中国剩余定理给出了模数两两互质的线性同余方程组的一个特解。方程组的通解可以表示为 \(x+km(k\in \mathbb{Z})\) 。有些题目要求我们求出最小的非负整数解,只需把 \(x\) 对 \(m\) 取模,并让 \(x\) 落在 \(0\) ~ \(m-1\) 的范围内即可。
另外,及时模数不满足两两互质,我们也有方法判断线性同余方程是否有解,并求出方程组的解。
本题中 \(m_i\) 不一定两两互质,中国剩余定理不再适用。可以考虑使用数学归纳法,假设已经求出了前 \(k-1\) 个方程构成的方程组的一个解 \(x\)。记 \(m = lcm(m_1,m_2, \cdots, m_k-1)\) ,则 \(x+i*m(i \in \mathbb{Z})\) 是前 \(k-1\) 个方程的通解。
以前两个方程举例说明:
则
于是问题转化为,在已知 \(a_1, a_2, m_2-m_1\) 的前提下,求一组 \(k_1, k_2\) 满足以上线性同余方程,而线性同余方程有解的充要条件是 \(gcd(a_1,a_2) | m_2 - m_1\) 。
根据扩展欧几里得算法,我们可以先求出线性同余方程 \(k_1'a_1 - k_2'a_2 = gcd(a_1, a_2)\) 的一组特解 \((k_1', k_2')\),再将方程两边同时乘以 \(\frac{m_2-m_1}{gcd(k_1,k_2)}\),即可得到方程 \(k_1a_1 - k_2a_2 = m_2 - m_1\) 的一组特解:
该方程的通解可以表示为:
记 \(gcd(a_1,a_2) = d\) ,于是
记 \(a_1k_1 + m_1 = m\) ,\(lcm(a_1,a_2) = a\),则我们将方程组内前两个方程合为一个方程:
对后续方程递归进行上述操作,即可将方程组合并为一个方程,最后得出:
#include <iostream>
using namespace std;
typedef long long LL;
// 扩展欧几里得算法,返回a与b的最大公约数,同时求出ax+by=gcd(a,b)的解(x,y)
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;
bool has_answer = true; // 代表是否有解
LL a1, m1; // 每次将一组新的方程合并入现有方程
cin >> a1 >> m1; //读入第一个方程
// 读入之后的n-1个方程
for (int i = 0; i < n - 1; i ++ ) {
LL a2, m2;
cin >> a2 >> m2;
LL k1, k2;
LL d = exgcd(a1, a2, k1, k2);
// 若不满足线性同余方程有解的充要条件 => gcd(a1,a2) | m2 - m1,则跳出循环,置为false
if ((m2 - m1) % d) {
has_answer = false;
break;
}
// 转换特解为通解 k1a1 - k2a2 = gcd(a1,a2) => k1a1 - k2a2 = m2 - m1
k1 *= (m2 - m1) / d;
LL t = a2 / d;
k1 = (k1 % t + t) % t; // 在转换过程中求出最小的非负整数解,防止溢出
// 更新合并后的a1 与 m1
m1 = a1 * k1 + m1;
a1 = abs(a1 / d * a2);
}
// C++ 取模可能得到负数,所以对取模的结果加m1再模m1,即可转为正数
if (has_answer) cout << (m1 % a1 + a1) % a1 << endl;
else puts("-1");
return 0;
}