【数论基础】中国剩余定理及其扩展

洛谷上的模板题

先讲讲中国剩余定理

  孙子定理是中国古代求解一次同余式组的方法。是数论中一个重要定理。又称中国余数定理。一元线性同余方程组问题最早可见于中国南北朝时期(公元5世纪)的数学著作《孙子算经》卷下第二十六题,叫做“物不知数”问题,原文如下:
  有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?即,一个整数除以三余二,除以五余三,除以七余二,求这个整数。《孙子算经》中首次提到了同余方程组问题,以及以上具体问题的解法,因此在中文数学文献中也会将中国剩余定理称为孙子定理。
上面是度娘的。将上面的例题转化成方程组是这样的:
 
假设n1,n2,n3分别是满足方程①,②,③的x的解,
如果有a%b=c,则有(a+k∗b)%b=c(k为非零整数) ,从n1角度考虑,当n2和n3都是3的整数倍时,n1+n2和n1+n2+n3都能够满足除以3余2。再从n2,n3角度考虑,得到:
为使n1+n2+n3的和满足除以3余2,n2和n3必须是3的倍数。
为使n1+n2+n3的和满足除以5余3,n1和n3必须是5的倍数。
为使n1+n2+n3的和满足除以7余2,n1和n2必须是7的倍数。
为使n1+n2+n3的和作为“孙子问题”的一个最终解,需满足:
n1除以3余2,且是5和7的公倍数。
n2除以5余3,且是3和7的公倍数。
n3除以7余2,且是3和5的公倍数。
因为如果a%b=c,那么(a∗k)%b=a%b+a%b+…+a%b=c+c+…+c=k∗c(k>0)。所有在求n1时,先找一个除以3余1的数,再乘以2。也就是先求出5和7的公倍数模3下的逆元,再用逆元去乘余数。n2与n3同理。
n1+n2+n3只是问题的一个解,并不是最小的解 ,最小解是(n1+n2+n3)%105 。
 
由上,可以推出中国剩余定理的通用算法:
设正整数m1,m2,m3…mk两两互素(注意这里),则同余方程组
有整数解。并且在模M= m1m2m3…mk下有唯一解x为:
其中,Mi=M/mi,Mi-1为Mi模mi的逆元。
 
例题代码:
#include<cstdio>
#include<iostream>
#include<cctype>
#include<algorithm>
#include<cmath>
using namespace std;
#define int long long

int n,x,y;
int m[15],r[15];

inline void ex_gcd(int a,int b){
    if(!b){x=1,y=0;return ;}
    ex_gcd(b,a%b);
    int t=x;x=y;y=t-a/b*y;
}

inline int CRT(int n,int m[],int r[]){
    int M=1,t=0;
    for(int i=1;i<=n;i++)    M*=m[i];
    for(int i=1;i<=n;i++){
        int w=M/m[i];
        ex_gcd(w,m[i]);
        t=(t+w*r[i]*x)%M;
    }
    return (t+M)%M;
}

signed main(void){
    cin>>n;
    for(int i=1;i<=n;i++)    cin>>m[i]>>r[i];
    cout<<CRT(n,m,r)<<endl;
    return 0;
}

—————————————————————分割线————————————————————————————————

 

上面提到,中国剩余定理只能用在模数互质时,那不互质时?

我们需要扩展它:

同样的题,设正整数m1,m2,m3…mk,两两之间不一定互质,则有同余方程组

先从两个方程分析: 

            

很显然,他们可以变成如下的不定方程:

              

变化一下:

            

(这里k2的正负性是无所谓的,因为待会要求的是k1)

然后我们这里用扩展欧几里得算法求出x的一个特解x0,得出以下几个式子并得出结论:

            

那么根据最后一个方程,可以得出思路,便是相邻两个m去进行处理,最后递推得出答案。

还是洛谷上的模板

#include<cstdio>
#include<iostream>
#include<cctype>
#include<algorithm>
#include<cmath>
using namespace std;
#define int long long 

inline int read(){
    int s=0;bool flag=true;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')flag=false;ch=getchar();}
    while(isdigit(ch)){s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return flag?s:-s;
}

inline void out_put(int x){
    if(x<0)    x=-x,putchar('-');
    if(x>9)    out_put(x/10);
    putchar(x%10+'0');
}

inline void print(int x){out_put(x),puts("");}

const int N=1e5+10;
int x,y; 
int n,mod[N],rem[N];

inline int slow_mult(int x,int y,int MOD){
    x=x%MOD,y=y%MOD;
    return ((x*y-(int)(((long double)x*y+0.5)/MOD)*MOD)%MOD+MOD)%MOD;
}

inline int ex_gcd(int a,int b){
    if(!b){x=1,y=0;return a;}
    int ans=ex_gcd(b,a%b);
    int t=x;x=y;y=t-a/b*y;
    return ans;
}

inline int ex_CRT(){
    for(int i=1;i<n;i++){
        int a=mod[i],b=mod[i+1],c=rem[i+1]-rem[i];
        int gcd=ex_gcd(a,b);    
//        if(c%gcd)    return -1;
//若c不能被gcd整除则无解 ,但这题保证有解
        a/=gcd,b/=gcd,c/=gcd;//除去公因数,保证三者互质
        x=(slow_mult(x,c,b)+b)%b; 
        mod[i+1]=mod[i]/gcd*mod[i+1];//下一次的mod是最小公倍数 
        rem[i+1]=((slow_mult(x,mod[i],mod[i+1])+rem[i])%mod[i+1]+mod[i+1])%mod[i+1];
        //下次的rem[i]是m[i]*x+rem[i] 
    }
    return rem[n];//结果递推到了最后一位 
}

signed main(void){
    n=read();
    for(int i=1;i<=n;i++)    mod[i]=read(),rem[i]=read();
    cout<<ex_CRT()<<endl;
    return 0;
}

具体递推过程可以手推一下

posted @ 2020-05-01 20:27  初学者Ming  阅读(437)  评论(0编辑  收藏  举报