【算法笔记】中国剩余定理(CRT)与扩展中国剩余定理(exCRT)

  • 本文总计约 8000 字,阅读大约需要 40 分钟
  • 警告!警告!警告!本文有大量的 LATEX 公式渲染,可能会导致加载异常缓慢!

三人同行七十稀,五树梅花廿一支。
七子团圆月正半,除百零五便可知。

— —明代数学家程大位对『物不知数』问题解法给出的歌诀

前言

我觉得可以不要前言 QwQ

问题引入

在我国古代的数学巨著《孙子算经》中,有这样一个名叫『物不知数』的问题:

今有物不知数,三三之数剩二,五五之数剩三,七七之数剩二,问物几何?

用现代的话翻译过来,就是求下面的关于 x 的同余方程组的解:

{x2(mod3),x3(mod5),x2(mod7).

当然,我们可以计算出来最小的 x=23,那么,对于任意一个下面这样的关于 x 的同余方程:

{xa1(modm1),(1)xa2(modm2),(2)xan(modmn).(n)

它一定有解吗?如果有解,能不能给出一个高效的算法,求出来满足该方程的最小整数解呢?

中国剩余定理(CRT)

像上面这样,解决多个线性同余方程组的问题,被称为中国剩余问题

在先秦时期,就有了类如『隔墙算』,『剪管术』,『韩信点兵』一类的问题,具体内容都是关于求上面这样线性同余方程组的数学问题。

然而,这类问题的文献记载,最早出现在南北朝时期的数学著作《孙子算经》中,作者将其称为『物不知数』问题,也就是题目引入中的那个问题。

后来,宋朝数学家秦九韶在他的数学著作《大衍章》中给出了比较系统的解答,并且给出了一个重要的定理,即所谓的中国剩余定理(Chinese Remainder Theorem,CRT,又称孙子定理):

设线性同余方程组:

{xa1(modm1),(1)xa2(modm2),(2)xan(modmn).(n)

如果 m1,m2,mn 两两互质,则该方程组一定有解,且在模 M=mi 意义下解唯一。

剩余定理对解的构造

『好的,我们已经知道了这个问题一定有解了,但是我们怎么样才能得到这个解呢?』

这个时候,我们就要请出我国古代的数学家,程大位了。他将『物不知数』问题的解法变成了一首四句歌诀:

三人同行七十稀,五树梅花廿一支。七子团圆月正半,除百零五便可知。

用现在的话来翻译一下,就是用 x 除以 3 所得的余数 2,乘以一个 70人同行七十稀);

加上 x 除以 5 所得的余数 3,乘上 21(即廿一树梅花廿一支);

再加上 x 除以 7 所得的余数 2,乘上 15(半个月即十五天子团圆月正半);

得到的结果为 2×70+3×21+2×15=233,最后对 105 取模,得到 308mod105=23,就是最终的答案了(除百零五便可知)。

这四句歌谣的确把『物不知数』问题算清楚了,但是,里面还有一些问题没有解决。

『对啊,105=3×5×7 尚且不论,那些 70,21,15 是怎么得出来的呢?难道是通过瞎蒙蒙出来的?』

我们就要考察一下这些数字的含义了。

对于一个线性同余方程组:

{xa1(modm1),xa2(modm2),xan(modmn).

既然 x 在模 M 意义下有唯一解,我们就构造一个和 M 相关的解吧!

定义 M=i=1nmiMi=Mmi,那么我们构造:

x=(a1M1t1+a2M2t2++anMntn)modM=(i=1naiMiti)modM.

其中,tiMi 在模 mi 意义下的逆元。

我们下面可以证明,x 为该同余方程的一个解:

对于第 i 个方程 xai(modmi),我们代入上式,得

a1M1t1+a2M2t2++anMntnai(modmi).

因为 M1,M2,Mi1Mi+1,Mi+2,,Mn 里都含有因子 mi,所以:

jiajMjtj0(modmi)

那么原式只剩下一个 xaiMiti(modmi) 了,我们又知道, tiMimi 意义下的逆元,有 Miti1(modmi),所以 xai(modmi) 成立。

综上,x=(i=1naiMiti)modM

注意到 mi 不一定是质数,所以我们求 Mimi 意义下的逆元需要用扩欧来完成。

代码

代码如下:

#include <iostream>
using namespace std;
const int MAXN = 100001; 

typedef long long ll;
 
int n;
ll a[MAXN], m[MAXN], M = 1;
ll ans; 

void exgcd(ll a, ll b, ll &x, ll &y) {  //扩欧,不解释 
	if(!b) {
		x = 1, y = 0; return ; 
	} 

	exgcd(b, a % b, y, x);
	y -= a / b * x; 
} 

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

	for(int i = 1; i <= n; ++i) {
		scanf("%lld%lld", &m[i], &a[i]); //代表方程 x = a[i] (mod m[i]) 
		M *= m[i];  //求出 M 
	} 

	for(int i = 1; i <= n; ++i) {
		ll xi, yi;

		exgcd(M / m[i], m[i], xi, yi);  //扩欧求逆元 
		xi = (xi % m[i] + m[i]) % m[i];  //要是正的 

		ans = (ans + a[i] * (M / m[i]) * xi) % M;  //一边加一边取模 
	} 

	printf("%lld", ans); 
	return 0;
}
//by CaO 

扩展中国剩余定理(exCRT)

CRT 可以解决当 m1,m2,,mn 两两互质时的情况,但是如果 m1,m2,,mn 不能做到两两互质,那么 CRT 的算法就束手无策了— —在求逆元的时候会出问题。

于是,exCRT 应运而生,它的思想不再是简单地构造一个解,而是通过合并两个同余方程的方式解答剩余问题的。

怎么合并呢

我们考虑下面的两个同余方程:

{xa1(modm1),xa2(modm2).

我们可以将其改写一下,就变成了:x=k1a1+m1=k2a2+m2,其中,k1,k2N

做个变形,我们就得到:a1k1a2k2=m2m1,这是一个关于 k1,k2 的不定方程,由 Bézout 定理,k1,k2 存在,当且仅当 gcd(a1,a2)(m2m1)

这样,我们就有了:

a1k1gcd(a1,a2)=m2m1gcd(a1,a2)+a2k2gcd(a1,a2).

它也可以表示为下式:

a1k1gcd(a1,a2)m1m2gcd(a1,a2)(moda2gcd(a1,a2)).

我们设 (a1gcd(a1,a2))1K(moda2gcd(a1,a2)),则上式可转化为:

k1Km1m2gcd(a1,a2)(moda2gcd(a1,a2)).

即:

k1=Km1m2gcd(a1,a2)+pa2gcd(a1,a2).

x=k1a1+m1,得:

x1=Km1m2gcd(a1,a2)+Kpa1a2gcd(a1,a2)+m1.

注意到 a1a2gcd(a1,a2)=lcm(a1,a2),上式写成同余式即:

xKm1m2gcd(a1,a2)+m1(modlcm(a1,a2)).

像这样,我们就把两个同余方程合并成了一个,如果是 n 个的话,像这样一个一个合并,就可以解决了!

代码

代码如下:

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 300001;
typedef __int128 ll;

int n;
ll a[MAXN], b[MAXN], xi, yi;

ll mul(ll A, ll B) {
    ll res = 0;
    while(B) {
        if(B & 1) res = res + A;
        B >>= 1, A = A + A;
    }
    return res;
}

ll exgcd(ll a, ll b, ll &x, ll &y) {
    if(!b) {
        x = 1, y = 0; return a;
    }
    
    ll ans = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return ans;
}

ll inv(ll a, ll b) {
    ll x, k, res = exgcd(a, b, x, k);
    ll t = b / res;
    
    return (x % t + t) % t;
}

int main(void) {
    scanf("%d", &n);
    
    for(int i = 1; i <= n; ++i) 
        scanf("%lld%lld", &a[i], &b[i]);
    
    ll r = exgcd(a[1], a[2], xi, yi);
    ll A = mul(a[1], a[2] / r);
    ll B = (inv(a[1] / r, a[2] / r) * (b[2] - b[1]) / r % (a[2] / r) + a[2] / r) % (a[2] / r) * a[1] + b[1];  //合并两个同余方程
    
    for(int i = 3; i <= n; ++i) {
        r = exgcd(A, a[i], xi, yi);
        B = (inv(A / r, a[i] / r) * (b[i] - B) / r % (a[i] / r) + a[i] / r) % (a[i] / r) * A + B;
        A = mul(A, a[i] / r);  //继续合并同余方程
    }
    
    printf("%lld", B);
    return 0;
}
//by CaO

应用

CRT 在多项式领域有一个非常重要的应用,就是辅助完成任意模数 NTT(MTT)。

传统的 NTT 可以解决多项式乘法问题,但是如果给出的模数不能写成 2ka+1 的形式,我们就不一定能找到模数的原根,也不一定能保证原根有成为单位根的价值。

我们一般可以选择三个模数 9982443531004535809469762049 三个质数(都可以写成 2ka+1 的形式,且 3 是它们的原根)分别作 NTT,并且使用 CRT 将答案合并起来。

这种做法,我们成为任意模数 NTT,是一种非常有用的多项式算法。我们会在后续的学习中讲到。

例题

本题目列表会持续更新

posted @   CaO氧化钙  阅读(381)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示