前言
在洛谷上,这是一个连模板都是省选/NOI-的题目,我也钻研了好几天,也许还没有达到通透的境界,但我还是想写一些我在学习过程中的一些感想。
用途
这个定理,其实不能说是一个定理,我觉得更像一个算法。
给出如下同余方程组,让你求方程的最小正数解:
x
≡
b
1
x≡b_1
x≡b1(mod
a
1
)
a_1)
a1)
x
≡
b
2
x≡b_2
x≡b2(mod
a
2
)
a_2)
a2)
……
x
≡
b
i
x≡b_i
x≡bi(mod
a
i
)
a_i)
ai)
算法
我们先从简单的着手,一步一步来。先看前两个方程。
根据第一个方程,我们可以设
x
=
a
1
∗
x
1
+
b
1
x=a_1*x_1+b_1
x=a1∗x1+b1。同理,根据第二个方程,我们可以设
x
=
a
2
∗
(
−
x
2
)
+
b
2
x=a_2*(-x_2)+b_2
x=a2∗(−x2)+b2。(
x
2
x_2
x2前面加个负号是为了方便下面的移项)
因为x是相等的,所以
a
1
∗
x
1
+
b
1
=
a
2
∗
(
−
x
2
)
+
b
2
a_1*x_1+b_1=a_2*(-x_2)+b_2
a1∗x1+b1=a2∗(−x2)+b2
移项得:
a
1
∗
x
1
+
a
2
∗
x
2
=
b
2
−
b
1
a_1*x_1+a_2*x_2=b_2-b_1
a1∗x1+a2∗x2=b2−b1
a
1
∗
x
1
′
+
a
2
∗
x
2
′
=
g
c
d
(
a
1
,
a
2
)
a_1*x_1'+a_2*x_2'=gcd(a_1,a_2)
a1∗x1′+a2∗x2′=gcd(a1,a2)的解是可以通过扩展欧几里得求出来的
如果
b
2
−
b
1
b_2-b_1
b2−b1不是
g
c
d
(
a
1
,
a
2
)
gcd(a_1,a_2)
gcd(a1,a2)的倍数,那么无解。
然后有
a
1
∗
x
1
′
∗
(
b
2
−
b
1
)
/
g
c
d
(
a
1
,
a
2
)
+
a
2
∗
x
2
′
∗
(
b
2
−
b
1
)
/
g
c
d
(
a
1
,
a
2
)
=
b
2
−
b
1
a_1*x_1'*(b_2-b_1)/gcd(a_1,a_2)+a_2*x_2'*(b_2-b_1)/gcd(a_1,a_2)=b_2-b_1
a1∗x1′∗(b2−b1)/gcd(a1,a2)+a2∗x2′∗(b2−b1)/gcd(a1,a2)=b2−b1
x
1
=
x
1
′
∗
(
b
2
−
b
1
)
/
g
c
d
(
a
1
,
a
2
)
,
x
2
=
x
2
′
∗
(
b
2
−
b
1
)
/
g
c
d
(
a
1
,
a
2
)
x_1=x_1'*(b_2-b_1)/gcd(a_1,a_2),x_2=x_2'*(b_2-b_1)/gcd(a_1,a_2)
x1=x1′∗(b2−b1)/gcd(a1,a2),x2=x2′∗(b2−b1)/gcd(a1,a2)
当然,用扩展欧几里得求出的只是一组解,而我们希望得到的是最小的那一组解,所以要调整一下
x
1
x_1
x1。
把
x
1
x_1
x1变成
(
x
1
%
a
2
+
a
2
)
%
a
2
(x_1\% a_2+a_2)\% a_2
(x1%a2+a2)%a2,这样
x
1
x_1
x1就是最小正数解了。
那么
x
=
(
a
1
∗
x
1
+
b
1
)
%
(
a
1
∗
a
2
/
g
c
d
(
a
1
,
a
2
)
)
x=(a_1*x_1+b_1)\% (a_1*a_2/gcd(a_1,a_2))
x=(a1∗x1+b1)%(a1∗a2/gcd(a1,a2))
记最终的答案为ans,
a
1
∗
a
2
/
g
c
d
(
a
1
,
a
2
)
a_1*a_2/gcd(a_1,a_2)
a1∗a2/gcd(a1,a2)为m,那么
a
n
s
≡
x
ans≡x
ans≡x(mod
m
m
m)
这样我们就构造出了一个新的同余方程,来代替这两个方程。
推向一般化,我们现在有这两个同余方程:
a
n
s
≡
x
ans≡x
ans≡x(mod
m
m
m)
a
n
s
≡
b
i
ans≡b_i
ans≡bi(mod
a
i
a_i
ai)
按照上面的步骤再走一遍。
根据第一个方程,设
a
n
s
=
m
∗
x
1
+
x
ans=m*x_1+x
ans=m∗x1+x。同理,根据第二个方程,设
a
n
s
=
a
i
∗
(
−
x
2
)
+
b
i
ans=a_i*(-x_2)+b_i
ans=ai∗(−x2)+bi。
那么
m
∗
x
1
+
x
=
a
i
∗
(
−
x
2
)
+
b
i
m*x_1+x=a_i*(-x_2)+b_i
m∗x1+x=ai∗(−x2)+bi
移项得:
m
∗
x
1
+
a
i
∗
x
2
=
b
i
−
x
m*x_1+a_i*x_2=b_i-x
m∗x1+ai∗x2=bi−x
令
z
=
b
i
−
x
z=b_i-x
z=bi−x,要是z很大或者是个大负数,那将会面临溢出的风险,所以要取模。但对什么取模呢?因为在后面
x
2
x_2
x2的值根本没用到,所以z可以对
a
i
a_i
ai取模,这样只对
x
2
x_2
x2的值有影响,对
x
1
x_1
x1并没有影响。
根据扩展欧几里得求出
m
∗
x
1
′
+
a
i
∗
x
2
′
=
g
c
d
(
m
,
a
i
)
m*x_1'+a_i*x_2'=gcd(m,a_i)
m∗x1′+ai∗x2′=gcd(m,ai)的一组解
如果
z
z
z不是
g
c
d
(
m
,
a
i
)
gcd(m,a_i)
gcd(m,ai)的倍数,那么无解。
然后有
m
∗
x
1
′
∗
z
/
g
c
d
(
m
,
a
i
)
+
a
i
∗
x
2
′
∗
z
/
g
c
d
(
m
,
a
i
)
=
b
i
−
x
m*x_1'*z/gcd(m,a_i)+a_i*x_2'*z/gcd(m,a_i)=b_i-x
m∗x1′∗z/gcd(m,ai)+ai∗x2′∗z/gcd(m,ai)=bi−x
那么
x
1
=
x
1
′
∗
z
/
g
c
d
(
m
,
a
i
)
x_1=x_1'*z/gcd(m,a_i)
x1=x1′∗z/gcd(m,ai),
x
2
=
x
2
′
∗
z
/
g
c
d
(
m
,
a
i
)
x_2=x_2'*z/gcd(m,a_i)
x2=x2′∗z/gcd(m,ai)(其实
x
2
x_2
x2无所谓,反正后面用不到)
再调整
x
1
x_1
x1,使之变为最小正数解:
x
1
=
(
x
1
%
a
i
+
a
i
)
%
a
i
x_1=(x_1\% a_i+a_i)\% a_i
x1=(x1%ai+ai)%ai
然后构造新的同余方程:
x
=
(
m
∗
x
1
+
x
)
%
(
m
/
g
c
d
(
m
,
a
i
)
∗
a
i
)
x=(m*x_1+x)\% (m/gcd(m,a_i)*a_i)
x=(m∗x1+x)%(m/gcd(m,ai)∗ai),
m
=
m
/
g
c
d
(
m
,
a
i
)
∗
a
i
m=m/gcd(m,a_i)*a_i
m=m/gcd(m,ai)∗ai
则新的同余方程组还是
a
n
s
≡
x
ans≡x
ans≡x(mod
m
m
m)(ans只是存在于我们脑袋中的一个量,最终的答案就是x)
由于数据范围非常大,上面的带取模的乘法最好用快速乘,不然可能会溢出。
具体实现看我代码:
#include<iostream>
#include<cmath>
#include<cstdio>
using namespace std;
int n;
long long a[100005],b[100005];
long long exgcd(long long a,long b,long long &x,long long &y)
{
if (b==0)
{
x=1;
y=0;
return a;
}
long long gcd=exgcd(b,a%b,x,y);
int t=x;
x=y;
y=t-a/b*y;
return gcd;
}
long long cheng(long long x,long long y,long long z)//快速乘
{
long long t=0;
x=(x%z+z)%z;//快速乘一定要加这两句话,因为x可能是负数
y=(y%z+z)%z;
while (x>0)
{
if (x%2==1) t=(t+y)%z;
y=y*2%z;
x/=2;
}
return t;
}
long long excrt()
{
long long x=b[1],x1,x2,m=a[1];
for (int i=2;i<=n;i++)
{
long long z=((b[i]-x)%a[i]+a[i])%a[i];//为避免溢出,要对a[i]取模
long long gcd=exgcd(m,a[i],x1,x2);//求m*x1+a[i]*x2=gcd(m,a[i])的一组解,gcd=gcd(m,a[i])
if (z%gcd!=0) return 0;//说明无解
x1=cheng(x1,z/gcd,a[i]);//x1=x1*z/gcd,同时调整x1为最小正数解
x=cheng(m,x1,m/gcd*a[i])+x;//构造新的同余方程
m=m/gcd*a[i];
x=x%m;//上上行加了x之后还要取一次模
}
return x;//x就是最终是答案
}
int main()
{
cin>>n;
for (int i=1;i<=n;i++)
{
scanf("%lld %lld",&a[i],&b[i]);
}
cout<<excrt();
}
总结
这虽然只是一个模板,但难度却也不小,我学了好久,总是因为一些取模而困惑,希望这一篇文章把细节都写到了。像这种数据规模很大的题目,每一步都要想想能不能取模,对什么取模。