中国剩余定理(CRT)及其扩展(EXCRT)详解
问题背景
孙子定理是中国古代求解一次同余式方程组的方法。是数论中一个重要定理。又称中国余数定理。一元线性同余方程组问题最早可见于中国南北朝时期(公元5世纪)的数学著作《孙子算经》卷下第二十六题,叫做“物不知数”问题,原文如下:
有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?即,一个整数除以三余二,除以五余三,除以七余二,求这个整数。《孙子算经》中首次提到了同余方程组问题,以及以上具体问题的解法,因此在中文数学文献中也会将中国剩余定理称为孙子定理。
用现代数学的语言来分析这个问题,中国剩余定理给出了以下的一元线性同余方程组:
在中国剩余定理(以下简称 ) 中
为两两互质的整数,在扩展中国剩余定理(以下简称
)中
并不满足互质条件,后者相对于前者更难解决。而两者都要运用一个算法来解决:扩展欧几里得算法。
扩展欧几里得算法
有啥用呢:
我们在中学就知道,计算两个正整数的最大公因数有两个比较常用的方法:更相减损术和辗转相除法,其中辗转相除法也叫欧几里得算法:(一些题外话:个人认为以人名来命名是一种非常原始和不科学的做法,我们无法从人名中提取到关于这个定理的信息。中文在这个时候就显得博大精深了,在定义一个定理的同时十分精炼地概括了定理的精华部分,实在是妙不可言)。而扩展欧几里得算法是欧几里得算法的扩展(废话),广泛应用于
加密等领域。在
问题中,我们用于求得逆元和求解裴蜀等式(后面会讲的),在
中我们用来求解最小公因数和裴蜀等式。
裴蜀等式:
裴蜀定理(或贝祖定理,同样是看名字一脸懵的定理)得名于法国数学家艾蒂安·裴蜀,说明了对任何整数
和它们的最大公约数
,关于未知数
和
的线性二元一次不定方程(称为裴蜀等式):一定存在整数
,使
成立。它的一个重要推论是:
互质的充要条件是存在整数
使
。证明我就略去了,来讲一下扩展欧几里得算法怎么得到裴蜀等式的一个解(有多个解,求出一个解可以写出通解)。
裴蜀等式求解过程:
应用欧几里得算法是一个递归的过程,将规模较大的问题转化为等价的小问题去解决,核心代码就是,证明也略去了(笔者数学一般,见谅)。它的边界情况出现在
的时候,这时
就是
啦。而扩展欧几里得算法则多了几个额外的步骤,在递归返回的时候做了一些小操作,使得我们可以得到
的一组解。思路如下:
第一层:
,递归到下一层,
, 同时向下一层传
和
,设为
和
第二层中:
,我们可以把
表示为
,代入整理可得
比较可得,第一层中的
和
和第二层中
和
的关系为
第二层的
和
又可以由第三层的
和
代入同样的求得,在递归终点的时,我们只需让
即可。注意因为上一层需要知道下一层的
和
,所以我们可以用传引用的方法,在递归返回时
和
就是下一层的
和
了。代码很短,但是不要写错细节哦(第二种写法更简洁)。
//求解 ax+by = gcd(a,b),注意是传引用 void exGcd(ll a,ll b,ll &x,ll &y){ if(b == 0) { x = 1,y = 0; return;} // b = 0时,a = gcd(原a,原b),返回 exGcd(b,a%b,x,y); ll temX = x; //保留下一层的x' x = y,y = temX - a/b * y; //x = y', y = x' - a/b * y' (x'和y'是递归下一层返回后的x和y) } /*void exGcd(ll a,ll b,ll &x,ll &y){ //更简洁的写法 if(b == 0) { x = 1,y = 0; return;} exGcd(b,a%b,y,x); //x和y换位 y = y- a/b*x; }*/
洛谷有道扩展欧几里得算法的模板题,充分理解后做出这题以后扩欧都难不住你
同余方程求解过程:
同余方程同样可以化成一个二元一次不定方程
,注意二元一次不定方程是不一定有整数解的,有整数解的充要条件是
,表示
是
的一个因数。我们设
,则我们先用扩展欧几里得算法解出翡蜀等式
的解,再乘上
即可得到同余方程的一个特解 (设为
)。要求通解也很简单,如下
其中, ,
证明当然也没有啦,可以手推一下的。
CRT问题的解决方法
构造出解
问题主要利用了
为两两互质的整数这一美好的性质,我们可以让
,同时定义
我们知道 是和
互质的(因为有两两互质这一性质保证)。再定义逆元:
为
模
意义下的数论倒数(
为
模
意义下的逆元,并不是真正的我们以前学的倒数),满足
,然后我们可以构造出一个解
,因为对于
:
所以它满足了 ,
,解是成立的。
通解就是 , 而
问题所求的最小整数解其实就是
。
逆元求法
求解逆元的方法主要有如下三种,本文只介绍使用扩展欧几里得的方法。
1、费马小定理,限制模数必须为质数。 问题中模数只是互质,不一定是质数,所以不可用。
2、欧拉定理,蒟蒻还不会。
3、扩展欧几里得,进入正题。
逆元构造的方程其实就是一个同余方程嘛(
和
互质,
,方程有整数解) ,代入扩展欧几里得模板就可以求出来了。
CRT问题完整代码
#include <bits/stdc++.h> using namespace std; typedef long long ll ; ll mod[15],yu[15],M = 1,ans;//mod[i]即为mi,yu[i]存放模后余数 void exGcd(ll a,ll b,ll &x,ll &y){ //求解 ax+by = gcd(a,b),注意是传引用 if(b == 0) { x = 1,y = 0; return;} // b = 0时,a = gcd(原a,原b) exGcd(b,a%b,x,y); ll temX = x; x = y,y = temX - a/b * y; //x = y', y = x' - a/b * y' (x'和y'是递归下一层返回后的x和y) } /*void exGcd(ll a,ll b,ll &x,ll &y){ //更简洁的写法 if(b == 0) { x = 1,y = 0; return;} exGcd(b,a%b,y,x); //x和y换位 y = y- a/b*x; }*/ int main() { int n; cin>>n; //方程组数 for (int i = 1; i <= n ; ++i) { scanf("%ld %ld",&mod[i],&yu[i]); //模数和余数,模数互质 M*=mod[i]; } for (int i = 1; i <= n ; ++i) { ll Mi = M / mod[i],inv,y; // Mi为所有模数乘积除以第i个模数,inv为Mi在模mi意义下的逆元 exGcd(Mi, mod[i], inv, y); inv = inv % mod[i]; ans = (ans + yu[i] * Mi * inv) % M; } cout<< (ans + M) % M; //保证结果不出现负数 return 0; }
题目链接 洛谷P1495 【模板】中国剩余定理(CRT)/曹冲养猪
前者是裸题,后者要稍作变换,并且要使用快速乘。
EXCRT问题的解决方法
为啥不能继续用CRT的代码解决问题了呢?
显然不能。因为同余方程组不再满足为两两互质的整数这一美好的性质了,上面求出的
与
不一定互质,从而导致了
这一条件失效,逆元也求不出来。所以我们不能再用逆元来解决 的问题,必须换一种思路。
合并同余方程组
观察一下几个简单的式子(摘自一位让我学会 的大佬 阮行止的博客,里面的数学证明表示更加严谨)
我们可以发现,同余方程在一定条件下是可以合并的,但是也会出现无解的情况。合并后的方程仍然可以表示为同余方程的形式,并且模数是原来模数的最小公倍数(上述这些规律其实是要证明的,但是蒟蒻不太会,感兴趣的同学可以自己研究一下上面大佬的博客和其他资料)。那么,解决 问题的关键,就在于如何合并这
个同余方程组,并判断是否有解。
合并流程
假设前 个同余方程合并得到的新方程为:
是前
个同余方程模数的最小公倍数,现在考虑合并第
个方程:
。
对于前 个同余方程,其通解为
, 其中
是我代码里面的
。我们在这个通解里找到一个
,使得
,即可以满足第
个方程。那么合并后前
个同余方程的通解就是
, 再对通解模
即可得到新的
作为前
个同余方程的最小整数解。具体实现的时候还要注意可能会出现负数和爆long long的情况,要用一些小技巧避免 。
找 的过程如下:方程整理为
,其中
就是翡蜀等式的
,
就是裴蜀等式的
,
要满足
,否则无解。我们先用老朋友扩欧算法来解出
,顺便求出
,再将得到的解
,即可求出
,注意这里得用快速(龟速)乘,会爆
,而且
不能是负数(否则快速乘会
) , 所以这里都要使用一些取模的技巧。之后要更新
和
,让
成为前
个同余方程模数的最下公倍数。这里有个公式是
,可以用它来更新
。
EXCRT 问题的完整代码
#include <bits/stdc++.h> using namespace std; typedef long long ll; ll n,mod[100009],yu[100009]; //要用快速乘(龟速乘),防止爆long long ll qMul(ll a,ll b,ll mo){ ll an = 0; while(b) { if(b&1) an =(an+a) % mo; a = (a+a)%mo; b>>=1; }return an%mo; } //扩展欧几里得算法,返回gcd(a,b),并计算出ax+by = gcd(a,b)中的x和y ll exGcd(ll a,ll b,ll &x,ll &y){ if( b == 0 ) { x = 1;y = 0; return a;} ll gcd = exGcd(b,a%b,y,x); //注意x和y的顺序 y = y - a/b*x; return gcd; } int main() { ios::sync_with_stdio(false);//加速cin和cout cin>>n; for(int i = 1;i <= n;i++) cin>>mod[i]>>yu[i]; ll ans = yu[1],M = mod[1] ,t,y; //ans表示前i-1个方程式的特解(余数),M为前i-1个方程式的模数的最小公倍数(i从2开始) for(int i = 2;i <= n;i++){ ll mi = mod[i],res = ((yu[i] - ans)%mi + mi)%mi; //res是等式的右边部分,不能出现负数 ll gcd = exGcd(M,mi,t,y); //求出gcd(mi,M) if(res % gcd != 0) { cout<<-1<<endl;exit(0); } //如果等式右边不能整除gcd,方程组无解 t = qMul(t,res/gcd,mi); //求出t还要乘上倍数,注意是快速乘取模mi (对谁取模要分清) ans += t * M; //得到前i个方程的特解(余数) M = mi /gcd * M; //M等于lcm(M,mi),注意乘法要在除法后面做,否则会爆long long ans = (ans%M+M)%M; //让特解范围限定在0~(M-1)内,防止会出现负数 } cout<<ans; return 0; }
裸题链接 洛谷P4777 【模板】扩展中国剩余定理(EXCRT)
两道题的唯一区别是:前者保证一定有解,后者不一定有解。
蒟蒻初学扩展欧几里得算法和和
问题,可能会出现一些笔误和逻辑错误,希望能得到指正。全文很多内容整理自一些大佬的博客和百度百科,旨在帮助从没有学过这些算法的同学在理解上提供尽可能多的帮助(我也是昨天才学的QAQ),如果有没有理解的地方可以私信我哦。如果有帮助,希望给我一个赞鼓励我(markdown使用过度,页面渲染好像出了点问题)
(@^_^@)