中国剩余定理#
《孙子算经》有云:
今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二,问物几何?
大意为:有 x 个物品,满足如下线性同余方程组:
⎩⎨⎧x≡2(mod3)x≡3(mod5)x≡2(mod7)刚入坑编程的估计在“循环结构”那一章里就写过求解这个方程的枚举代码了。这种方式不好的一点就是码量随方程数的增多而膨胀(但是你可以写一个生成计算代码的程序,然后运行编译后的程序)。我们急需一种通解,在码量不增的情况下实现对这个方程组的求解。
我们考虑拆开这个方程组,分别令 x1,x2,x3 满足:
⎩⎨⎧x1mod3=2x2mod5=3x3mod7=2两个方程组的写法其实是等价的,只是把同余转化为了带余除法罢了。我们惊奇地发现,如果再加上几个约束条件,原方程组的解 x 就可以经过 x1,x2,x3 表示出来:
⎩⎨⎧x1mod35=0x2mod21=0x3mod15=0此时 x=x1+x2+x3,因为 (x1+x2+x3)mod3=x1mod3+x2mod3+x3mod3=2+0+0=2,其他同理。
而我们的中国剩余定理断言道,对于模数均互质的 n 元线性同余方程组:
⎩⎨⎧x1≡a1(modM1)x2≡a2(modM2)x3≡a3(modM3)⋮xn≡an(modMn)它的解 x≡i=1∑naimimi−1(modM),其中 M=i=1∏nMi,mi=MiM(modMi)。因此只需要计算同余号右侧的部分即可,由于方程组模数互质并不等于每个方程的模数是质数,因此需要用到扩展欧几里得算法来求解逆元。
ll exgcd(ll a, ll b, ll &x, ll &y) {
ll d = exgcd(b, a % b, y, x);
return (ans % p + p) % p;
for (int i = 1; i <= n; i++) M *= m[i];
for (int i = 1; i <= n; i++) {
res += a[i] * Mi * inv(Mi, m[i]);
扩展中国剩余定理#
经历过数论大风大浪的应该都知道,马上要开始推导模数不全互质的情况了。
还是那个方程组:
⎩⎨⎧x1≡a1(modM1)x2≡a2(modM2)x3≡a3(modM3)⋮xn≡an(modMn)此时不保证 M1∼Mn 两两互质,求出方程的最小正整数解。此时因为失去了互质,构造通解的方式已经不能用了。因此我们转而寻找另外的方法来简化这个方程组。
对大量事实的观测表明,两个线性同余方程在合并后遵循如下性质:
- 模数变为原两个方程模数的最小公倍数
- 合并得到的方程与原方程同型
- 合并得到的方程不一定有解
也即:
{x≡4(mod6)x≡3(mod5){x≡2(mod4)x≡3(mod6)⇒x≡28(mod30)⇒∅事实上,假设我们有方程 x≡a1(modm1) 和 x≡a2(modm2)。那么转化成带余方程之后就有:
xk1m1+a1k1m1−k2m2m1k1−m2k2=k1m1+a1=k2m2+a2=k2m2+a2=a2−a1=a2−a1观察到这是二元一次不定方程 ax+by=c 的形式,根据贝祖定理,有解的充要条件是 gcd(m1,m2)∣(a2−a1),因此无解情况就可以筛去了。
约去最大公倍数得:gcd(m1,m2)m1k1−gcd(m1,m2)m2k2=gcd(m1,m2)a2−a1。用扩展欧几里得解出方程 m1t1+m2t2=1 的特解 t1,t2,那么 k1,k2 可以用如下关系得到:
{k1=t1gcd(m1,m2)a2−a1k2=−t2gcd(m1,m2)a2−a1于是 x=k1m1+a1=m1t1gcd(m1,m2)a2−a1+a1。用 k2 去代结果是一样的。
那么对于方程
{x≡a1(modm1)x≡a2(modm2)仅当 gcd(m1,m2)∣(a2−a1) 时有整数解 x=m1t1gcd(m1,m2)a2−a1+a1。那么方程的通解可以表示成 x+λlcm(m1,m2),λ∈Z 的形式,这样就能解释合并后方程的模数为什么为最小公倍数了。
我们假设 0≤p≤q<lcm(m1,m2),并假设存在关系:
{p≡a1(modm1)p≡a2(modm2){q≡a1(modm1)q≡a2(modm2)做差可得:
{q−p≡0(modm1)q−p≡0(modm2)得到关系 m1∣(q−p),m2∣(q−p)⇒lcm(m1,m2)∣(q−p)。注意到两个数的最小公倍数一定满足 lcm(a,b)≥max(a,b)。但是对于非负整数 p,q 来说,q−p≤max(p,q) 是一定成立的,且在 p=q 时取得等号,那么当且仅当在 p=q 时才存在最小公倍数的整除关系。因此导出式是一定正确的。
总结一下,对于方程组:
{x≡a1(modm1)x≡a2(modm2)有解的充要条件是 gcd(m1,m2)∣(a2−a1)。若已知有解,假设二元一次不定方程 m1t1+m2t2=1 的特解是 t1,t2,并记 S=lcm(m1,m2)=gcd(m1,m2)m1m2,D=gcd(m1,m2),那么原方程等价于:
x≡m1t1Da2−a1+a1(modS)ll exgcd(ll a, ll b, ll &x, ll &y) {
ll d = exgcd(b, a % b, y, x);
ll mul(ll a, ll b, ll p) {
// 著名的龟速乘,就是把快速幂中的乘号改成加号,用来在不爆范围的情况下计算乘积取模的结果
bool flag = (b < 0) ^ (a < 0); // 处理负数情况,同号相乘为正,异号为负
// 注意到:a * (-b) = -(a * b),数字按绝对值相乘,负号最后处理
if (b & 1) res = (res + a) % p;
return res * (flag ? -1 : 1) % p; // 处理负号
Equation cur = stk.top();
Equation nxt = stk.top();
ll D = gcd(cur.m, nxt.m);
ll S = lcm(cur.m, nxt.m);
if ((nxt.a - cur.a) % D != 0) return -1; // 无解判断
exgcd(cur.m, nxt.m, t1, t2); // 求解特解
ll X = ((mul(mul(cur.m, t1, S), (nxt.a - cur.a) / D, S)) + cur.a) % S;
stk.push((Equation) {X, S});
Equation res = stk.top();
return (res.a % res.m + res.m) % res.m; // 转为正整数
ios::sync_with_stdio(false);
for (int i = 1; i <= n; i++) cin >> a[i] >> b[i];
for (int i = 1; i <= n; i++) stk.push((Equation) {b[i], a[i]});