这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战.
欧几里得算法euclid
欧几里得算法又称辗转相除法,可以快速求得两个数的最大公约数。
原理
- gcd(a,b)=gcd(b,amodb)
- gcd(a,0)=a
证明
符号约定:x∣y表示x能够整除y,即y是x的倍数。
- 设(a,b)的任一公约数为k,即k∣a且k∣b
- 设a=xb+y,则有amodb=y
- 因为k∣b,所以k∣xb,又因为k∣xb+y,所以k∣y
- 所以(a,b)的任一公约数均为(b,amodb)的公约数
- 同理可证,(b,amodb)的任一公约数均为(a,b)的公约数。
- 所以(a,b)和(b,amodb)的公约数集合相同,显然最大公约数相同。
代码
递归写法
int euclid(int a, int b)
{
return b ? euclid(b,a%b) : a;
}
递推写法
int euclid(int a, int b)
{
while(b)
{
int k = a%b;
a = b;
b = k;
}
return a;
}
时间复杂度
算法每次迭代会将(a,b)变为(b,amodb),直到b为0为止。
由于amodb是小于a/2的,所以每次迭代都会至少把值减少一半。
所以时间复杂度在O(log(max(a,b))内,实际中往往会更低。
其它
euclid方法求gcd主要是学原理以及为了后面的扩展欧几里得方法做铺垫,实际中求gcd可以使用库函数.
int __gcd(int a, int b);
扩展欧几里得算法exeuclid
求二元一次方程ax+by=c的整数通解.
引理
贝祖定理(裴蜀定理):方程ax+by=c有解的充要条件是:gcd(a,b)∣c
口头证明如下:
必要性:c必须是gcd(a,b)的倍数,否则方程必无解。
充分性:ax+by=gcd(a,b)有解,对于任意gcd(a,b)的倍数c,只要将x,y都乘以c/gcd(a,b)即可.
原理
- 对于两方程ax1+by1=gcd(a,b),bx2+(amodb)y2=gcd(a,b),它们解的关系是:x1=y2,y1=x2−⌊a/b⌋y2。
- 边界情况a=gcd(a,b),b=0,方程解显然为x=1,y=0。
- 将原始方程不断通过类似于辗转相除的方式得到边界方程,就可以一步一步反推到原始方程的解。
证明
- 现有两方程ax1+by1=gcd(a,b),bx2+(amodb)y2=gcd(a,b)
- 取相等得到ax1+by1=bx2+(amodb)y2
- 把amodb=a−⌊a/b⌋∗b代入
- 得到ax1+by1=bx2+(a−⌊a/b⌋∗b)y2
- 整理得x1a+y1b=y2a+(x2−⌊a/b⌋y2)b
- 由恒等定理,x1=y2,且y1=x2−⌊a/b⌋y2
就得到了方程之间的解的转化关系。
代码
返回方程ax+by=gcd(a,b)的一个解(x,y)
pair<int,int> exeuclid(int a, int b)
{
if(b==0) return {1,0};
auto p = exeuclid(b, a%b);
return {p.second, p.first-a/b*p.second};
}
其它
按以上代码得到的解是ax+by=gcd(a,b)的解,如果要得到ax+by=c的解,需要将x和y都乘上c/gcd(a,b)
使用exeuclid求逆元
给定a,m,要求满足ax≡1(modm)的x,即inv(a,m).
即,解二元方程ax+my=1获得一个x,再将它通过加减m放到[0,m)区间内即可。
见 luogu P1082
例题
Luogu P1082 同余方程
求关于 x的同余方程ax≡1(modm)的最小正整数解。
使用exeuclid求逆元即可。
int inv(int a, int m)
{
int x = exeuclid(a, m).first;
x = (x%m+m)%m;
return x;
}
int main(void)
{
int a = read(), b = read();
printf("%d\n",inv(a,b) );
return 0;
}
Luogu P1516 青蛙的约会
青蛙A初始在数轴x处,每秒走m;青蛙B初始在数轴y处,每秒走n;数轴长为L,首尾相接。问第几秒时两只青蛙在同一个整点处,如果无解输出Impossible.
需要解同余方程(m−n)∗t≡y−x(modL)
即解二元方程(m−n)∗t+L∗t′=y−x
ll a=m-n, b=L, c=y-x;
if(c % __gcd(a,b))
printf("Impossible\n");
else
ll t = exeuclid(a, b).first * (c/__gcd(a,b));
求出一个解后,需要得到最小的正整数解。先找到解的循环周期T=lcm(L,m−n)/(m−n),再将t放到[0,T)内,完整代码如下:
int main(void)
{
ll x=read(), y=read(), m=read(), n=read(), L=read();
ll a=m-n, b=L, c=y-x;
if(c % __gcd(a,b))
printf("Impossible\n");
else
{
ll t = exeuclid(a, b).first * (c/__gcd(a,b));
ll T = abs(L/__gcd(L,(m-n)));
t = (t%T+T)%T;
cout << t << endl;
}
return 0;
}
本文也发表于我的 CSDN 博客中。