算法学习笔记(9): 中国剩余定理(CRT)以及其扩展(EXCRT)

扩展中国剩余定理

讲解扩展之前,我们先叙述一下普通的中国剩余定理

“China Remain Theory” 也叫做孙子定理

难得是以中国命名的定理,谁敢不掌握

中国剩余定理

中国剩余定理通过一种非常精巧的构造求出了一个可行解

但是毕竟是构造,所以相对较复杂

{xa1(modm1)xa2(modm2)xa3(modm3)xan(modmn)

对于上述同余方程组,首先需要满足 m1,m2,m3,,mn 互质

m=i=1nmi,Mi=m/mi, ti 是线性同余方程 Miti1(modmi) 的一个解

那么上述方程组有整数解,为 x=i=1naiMiti


证明:

由于所有 mi 互质,所以 Mi=m/mi 是除了 mi 之外的所有模数的倍数,所以 ji,aiMiti0(modmk)

所以代入 x=i=1naiMiti,原方程组成立


中国剩余定理给出了方程的一个特解。通解可以表示为 x+km(kZ)。如果需要求最小的非负整数解,只需要让 x 对于 m 取模就是。

参考代码

【模板】中国剩余定理(CRT)/ 曹冲养猪 - 洛谷

#include <iostream>

typedef long long ll;

ll m[11], a[11], Mm[11], y[11];

// 扩展欧几里得算法,由于求出线性同余方程 Mi * ti = 1 (mod p) 的一个特解
// 上述同余式可以转化为 Mi * ti + k * p = 1
// 所以跑一个 extgcd(Mi, p, ti, k) 即可 (k没有用的)
void extgcd(ll a, ll b, ll &x, ll &y) {
    if (b == 0) {
        x = 1, y = 0;
        return; 
    }
    extgcd(b, a % b, y, x);
    y -= (a / b) * x;
}

int main() {
    int n; scanf("%d", &n);

    ll M(1);
    // 这里读入,并求出m
    for (int i = 0; i < n; i++) {
        scanf("%lld%lld", m + i, a + i);
        M *= m[i];
    }

    // 求出Mi
    for (int i = 0; i < n; i++) {
        Mm[i] = M / m[i];
    }

    // 求出每一个不定方程的特解
    for (int i = 0; i < n; i++) {
        ll x, tmp, mi = m[i];
        extgcd(Mm[i], mi, x, tmp);
        y[i] = (x + mi) % mi;
    } 

    // 求和获得最小正整数解
    ll x(0);
    for (int i = 0; i < n; i++) {
        x = (x + a[i] * Mm[i] * y[i]) % M;
    }

    printf("%lld\n", x);
    return 0;
}

扩展中国剩余定理

还是考虑上述式子

{xa1(modm1)xa2(modm2)xa3(modm3)xan(modmn)

此时,我们不保证 mi 不一定两两互质,所以中国剩余定理不再适用

所以扩展中国剩余定理就开始发挥作用了

虽然说是扩展……但是两者思路很不一样

扩展中国剩余定理采用数学归纳法,或者说两两合并法

假如我们需要合并方程

xa1(modm1)

xa2(modm2)

我们将同余式改写

k1m1+a1=x

k2m2+a2=x

将两者相减,整理后可得

k1m1k2m2=a2a1

由于我们已知 m1,m2,a1,a2,也就是说我们只需要知道 k1,k2 其中任意一个,就可以求出这时的 x 是个什么东西了

所以,联想到了……扩展欧几里得算法

可以参考文章:算法学习笔记(1): 欧几里得算法及其扩展

我们令

d=gcd(m1,m2)c=a2a1

那么化简一下有:k1m1k2m2=c

所以我们求出 k1m1d+k2m2d=1

那么换回去之后

k1=k1cdk2=k2cd

由于 exgcd(a,b,x,y) 求出的是 xa+yb=gcd(a,b) 的一组解……所以需要 cd

l=lcm(m1,m2)=m1m2/gcd(m1,m2)

那么此时 xk1m1cd+a1(modl)

也就是可以得到 x 的一个解集 xX={kl+x|kR}

也就是说,我们将上述两个式子合并成了

xk1cdm1+a1(modlcm(m1,m2))

那么考虑代码怎么写

typedef long long lint;

struct Equation {
    lint a, p;
};

inline lint gcd(lint x, lint y) {
    return y ? gcd(y, x % y) : x;
}

inline lint exgcd(lint x, lint y, lint &s, lint &t) {
    if (!y) {
        s = 1, t = 0;
        return x;
    }
    lint r = exgcd(y, x % y, t, s);
    t -= (x / y) * s;
    return r;
}

bool merge(const Equation &a, const Equation &b, Equation &res) {
    // 第一部分:预先求出需要的值
    lint d = gcd(a.p, b.p), s, t;
    lint lcm = a.p / d * b.p;

    // 第二部分:求出上文中的 k1', k2'
    lint c = (b.a - a.a) % lcm;
    if (c < 0) c += lcm;
    if (c % d) return false; // 判断有无解,返回false则无解
    exgcd(a.p / d, b.p / d, s, t);

    // 第三部分:求出上文中的 k1
    s = s * (c / d) % lcm;
    if (s < 0) s += lcm;

    // 第四部分:求出一个特殊的解
    lint x = (a.a + s * a.p % lcm) % lcm;
    if (x < 0) x += lcm;
    res = {x, lcm};
    return true;
}

?:为什么在前三个部分放在了模 lcm 的情况下计算

其实最小的正整数解在 modlcm 的情况下是唯一的~

唯一性的证明可以参考: 阮行止 的博客

所以放在modlcm 的条件下不会对合并造成影响,并且还保证了求出的解是最小非负整数解。

?: 为什么在第一部分需要提前算出 gcd(m1,m2)

其实是不用的……只需要 data s, t; data d = exgcd(m1, m2, s, t);也可以算出正确答案……

?: 判断有解是如何判断的

有无解实际上就是拓展欧几里得算法有无解

而判断拓欧算法有无解,也就通过贝祖定理验证

也就是说,对于不定方程 ax + by = c

如果 gcd(x, y) | c 则有解,否则无解

【模板】扩展中国剩余定理(EXCRT) - 洛谷

NOTICE:在模板题上,由于可能会溢出……至于什么地方需要用龟速乘,请读者自行揣度

关于龟速乘:算法学习笔记(2): 逆元及其应用我提过一点

更详细的参考 快速乘总结 - 一只不咕鸟

后记

后来才发现,原来写的都是在胡扯……所以重新写了 exCRT 部分。好多都被网上的一些文章误导了,他们在 exCRT 前三个部分很多都是在modp2 意义下进行的。虽然这样无可厚非,但是在判断有无解时会出现问题,所以……

posted @   jeefy  阅读(279)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示