问题:
在《孙子算经》中有这样一个问题:“今有物不知其数,三三数之剩二(除以3余2),五五数之剩三(除以5余3),七七数之剩二(除以7余2),问物几何?”这个问题称为“孙子问题”,该问题的一般解法国际上称为“中国剩余定理”。
解析:
题目意思即为有这样一组方程:
(m1---mn两两互质)
设:
设:
设: 所以:
所以方程有解为:
查找modM意义下,或最小正整数解:方程有唯一解:
注释:假设整数m1,m2,m3...mn两两互质,则对于任意整数a1,a2,a3...an,x有解
例:
对于
x % 3 = 2
x % 5 = 3
x % 7 = 2
我们需要构造一个答案 //inv为求逆元
5*7*inv(5*7, 3) % 3 = 1
3*7*inv(3*7, 5) % 5 = 1
3*5*inv(3*5, 7) % 7 = 1
然后两边同乘你需要的数,得
2 * 5*7*inv(5*7, 3) % 3 = 2
3 * 3*7*inv(3*7, 5) % 5 = 3
2 * 3*5*inv(3*5, 7) % 7 = 2
令
a = 2 * 5*7*inv(5*7, 3)
b = 3 * 3*7*inv(3*7, 5)
c = 2 * 3*5*inv(3*5, 7)
那么
a % 3 = 2
b % 5 = 3
c % 7 = 2
其实答案就是a+b+c
因为
a%5 = a%7 = 0 //a是5*7*inv(5*7,3)的倍数,所以模5和7都是
b%3 = b%7 = 0 //同理,b,c模3/7,3/5都是0
c%3 = c%5 = 0
所以
(a + b + c) % 3 = (a % 3) + (b % 3) + (c % 3) = 2 + 0 + 0 = 2
(a + b + c) % 5 = (a % 5) + (b % 5) + (c % 5) = 0 + 3 + 0 = 3
(a + b + c) % 7 = (a % 7) + (b % 7) + (c % 7) = 0 + 0 + 2 = 2
即a+b+c是一个满足条件得答案 //最小得正整数解为(a+b+c)%105(105=3*5*7,两两互质,105为最小公倍数)
代码:
#include <algorithm> #include <iostream> #include <cstring> #include <cstdlib> #include <vector> #include <cstdio> #include <queue> #include <cmath> #include <map> #include <set> using namespace std; typedef long long ll; const int maxn=1e5+10; ll a[maxn],m[maxn],n; ll ex_gcd(ll a,ll b,ll &x,ll &y){ if(b==0){x=1;y=0;return a;} ll ans=ex_gcd(b,a%b,x,y); ll temp=x; x=y; y=temp-a/b*y; return ans; } ll inv(ll a,ll b){ //求逆 ll x,y; ll ans=ex_gcd(a,b,x,y); if(ans!=1)return -1; if(x<0)x=(x%b+b)%b; return x; } ll China(){//中国剩余定理 ll M=1; for(int i = 0;i<n;i++){ M*=m[i]; } ll sum=0; for(int i=0;i<n;i++){ ll res=M/m[i]; sum=(sum+a[i]*res*inv(res,m[i]))%M; } return sum; } int main(){ while(scanf("%lld",&n)!=EOF){ for(int i=0;i<n;i++){ scanf("%lld%lld",&m[i],&a[i]); } ll ans=China(); printf("%lld\n",ans); } return 0; }
上述方法只能解决m两两互质的同余方程组,如果不互质呢?
法一:
假设只有两个方程,如果能用一个方程代替两个方程,并能得到这个方程的解,那么这个问题就解决了:
x=a1+ k1*b1 (x%b1= a1)
x=a2+ k2*b2 (x%b2 = a2)
则: a1l+ k1*b1= a2 + k2*b2
then: b1* k1 + b2*(-k2)= a2-a1
假设gcd(b1,b2)= g若(a2 - a1)%g!=0 :无解
否则:k1*b1= (a2-a1) + k2*b2
then: k1*b1/g = (a2-a1)/g + k2*b2/g
then: k1*b1/g = (a2-a1)/g (mod b2/g)
then : k1 = inv(b1/g,b2/g)*(a2-a1)/g (mod b2/g)
then:x = a1 + inv(b1/g,b2/g)*(a2-a1)/g*b1 + b2*b1/g*y
then :x = c(mod m)
中:c= a1 + inv(b1/g,b2/g)*(a2-a1)/g*b1
m= b1*b2/g = lcm(b1,b2)
当作一个新方程继续与下一个联立,直到剩下最后一个方程xn = cn(mod mn)则,
任意一个xn都是满足条件的解,取最小正整数即可ans = (cn%mn + mn)%mn
代码:
#include <algorithm> #include <iostream> #include <cstring> #include <cstdlib> #include <vector> #include <cstdio> #include <queue> #include <cmath> #include <map> #include <set> using namespace std; typedef long long ll; const int maxn=1e5+10; ll C[maxn],M[maxn]; ll n,k; ll gcd(ll a,ll b){ return b==0?a:gcd(b,a%b); } ll ex_gcd(ll a,ll b,ll &x,ll &y){ if(b==0){x=1;y=0;return a;} ll g=ex_gcd(b,a%b,x,y); ll temp=x; x=y; y=temp-a/b*y; return g; } ll inv(ll a,ll mod){ ll X,Y; ll g=ex_gcd(a,mod,X,Y); if(g!=1)return -1; return (X%mod+mod)%mod; } /* ll mul(ll a,ll b,ll mod){ //快速乘法 ll ans=0; while(b){ if(b&1)ans=(ans%mod+a%mod)%mod; b>>=1; a=(a%mod+a%mod)%mod; } return ans; } */ int main(){ while(scanf("%lld",&n)!=EOF){ for(int i = 0;i<n;i++){ scanf("%lld%lld",&M[i],&C[i]); } bool flag=true; for(int i=1;i<n;i++){ ll M1=M[i-1],M2=M[i],C1=C[i-1],C2=C[i]; ll g=gcd(M1,M2); if((C2-C1)%g){flag = false;break;} M[i]=M1/g*M2; //可能会爆 ll INV=inv(M1/g,M2/g); if(INV==-1){flag=false;break;} C[i]= C1+(INV*((C2-C1)/g))%(M2/g)*M1; C[i]=(C[i]%M[i]+M[i])%M[i]; } if(!flag)printf("-1\n"); else printf("%lld\n",C[n-1]); } return 0; }
法二:(仅限m比较小的时候/牛客2019夏多校1-d)
注意构造前i个方程的最小自然数解,假设前i-1个方程的最小自然数解为f(i-1)。lcm(p1,p2,……,pi-1)=y,令f(i)=f(i-1)然后让f(i)每次加y,知道满足第i个方程即可。可以证明在保证有解的情况下,加法运算不会超过pi次。(所以要先判断方程组有无解)
#include <algorithm> #include <iostream> #include <cstring> #include <cstdlib> #include <vector> #include <cstdio> #include <queue> #include <cmath> #include <map> #include <set> using namespace std; typedef long long ll; const int maxn=100+10; ll ans,n,a[maxn],p[maxn],base,M; const char *definitely_lie="he was definitely lying"; const char *probably_lie="he was probably lying"; int main(){ scanf("%lld%lld",&n,&M); for(int i=0;i<n;i++)scanf("%lld%lld",&p[i],&a[i]); for(int i=0;i<n;i++){ for(int j=i+1;j<n;j++){ ll d=__gcd(p[i],p[j]); if(d!=1){ if(a[i]%d!=a[j]%d){ puts(definitely_lie); return 0; } } } } ans=0; base=1; for(int i=0;i<n;i++){ while(ans%p[i]!=a[i]){ ans+=base; if(ans>M){ puts(probably_lie); return 0; } } ll gg=p[i]/__gcd(base,p[i]); if(M/base>=gg)base*=gg; else{ for(int j=i+1;j<n;j++)if(ans%p[j]!=a[j]){ puts(probably_lie); return 0; } printf("%lld\n",ans); return 0; } } return 0; }