欧几里得算法与扩展欧几里得算法

281 阅读3分钟

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战.


欧几里得算法euclid

欧几里得算法又称辗转相除法,可以快速求得两个数的最大公约数。

原理

  1. gcd(a,b)=gcd(b,amodb)gcd(a,b) = gcd(b,a\bmod b)
  2. gcd(a,0)=agcd(a,0) = a

证明

符号约定:xyx|y表示xx能够整除yy,即yyxx的倍数。

  1. (a,b)(a,b)的任一公约数为kk,即kak|akbk|b
  2. a=xb+ya=xb+y,则有amodb=ya\bmod b = y
  3. 因为kbk|b,所以kxbk|xb,又因为kxb+yk|xb+y,所以kyk|y
  4. 所以(a,b)(a,b)的任一公约数均为(b,amodb)(b,a\bmod b)的公约数
  5. 同理可证,(b,amodb)(b, a\bmod b)的任一公约数均为(a,b)(a,b)的公约数。
  6. 所以(a,b)(a,b)(b,amodb)(b, a\bmod b)的公约数集合相同,显然最大公约数相同。

代码

递归写法

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)(a,b)变为(b,amodb)(b,a\bmod b),直到b为0为止。 由于amodba \bmod b是小于a/2a/2的,所以每次迭代都会至少把值减少一半。 所以时间复杂度在O(log(max(a,b))O(log(max(a,b))内,实际中往往会更低。

其它

euclid方法求gcd主要是学原理以及为了后面的扩展欧几里得方法做铺垫,实际中求gcd可以使用库函数.

int __gcd(int a, int b);

扩展欧几里得算法exeuclid

求二元一次方程ax+by=cax+by=c的整数通解.

引理

贝祖定理(裴蜀定理):方程ax+by=cax+by=c有解的充要条件是:gcd(a,b)cgcd(a,b)|c 口头证明如下: 必要性:cc必须是gcd(a,b)gcd(a,b)的倍数,否则方程必无解。 充分性:ax+by=gcd(a,b)ax+by=gcd(a,b)有解,对于任意gcd(a,b)gcd(a,b)的倍数cc,只要将x,yx,y都乘以c/gcd(a,b)c/gcd(a,b)即可.

原理

  1. 对于两方程ax1+by1=gcd(a,b)ax_1+by_1=gcd(a,b)bx2+(amodb)y2=gcd(a,b)bx_2+(a\bmod b)y_2=gcd(a,b),它们解的关系是:x1=y2x_1=y_2y1=x2a/by2y_1=x_2-\lfloor a/b\rfloor y_2
  2. 边界情况a=gcd(a,b),b=0a=gcd(a,b),b=0,方程解显然为x=1,y=0x=1,y=0
  3. 将原始方程不断通过类似于辗转相除的方式得到边界方程,就可以一步一步反推到原始方程的解。

证明

  1. 现有两方程ax1+by1=gcd(a,b)ax_1+by_1=gcd(a,b)bx2+(amodb)y2=gcd(a,b)bx_2+(a\bmod b)y_2=gcd(a,b)
  2. 取相等得到ax1+by1=bx2+(amodb)y2ax_1+by_1 = bx_2+(a\bmod b)y_2
  3. amodb=aa/bba\bmod b = a-\lfloor a/b\rfloor*b代入
  4. 得到ax1+by1=bx2+(aa/bb)y2ax_1+by_1 = bx_2+(a-\lfloor a/b\rfloor*b)y_2
  5. 整理得x1a+y1b=y2a+(x2a/by2)bx_1a+y_1b = y_2a+(x_2-\lfloor a/b\rfloor y_2)b
  6. 由恒等定理,x1=y2,且y1=x2a/by2x_1=y_2,且y_1=x_2-\lfloor a/b\rfloor y_2

就得到了方程之间的解的转化关系。

代码

返回方程ax+by=gcd(a,b)ax+by=gcd(a,b)的一个解(x,y)(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=gcd(a,b)的解,如果要得到ax+by=cax+by=c的解,需要将xxyy都乘上c/gcd(a,b)c/gcd(a,b)

使用exeuclid求逆元

给定a,ma,m,要求满足ax1(modm)ax\equiv 1\pmod mxx,即inv(a,m)inv(a,m). 即,解二元方程ax+my=1ax+my=1获得一个xx,再将它通过加减mm放到[0,m)区间内即可。 见 luogu P1082

例题

Luogu P1082 同余方程

求关于 xx的同余方程ax1(modm)ax\equiv 1\pmod m的最小正整数解。

使用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.

需要解同余方程(mn)tyx(modL)(m-n)*t\equiv y-x \pmod L 即解二元方程(mn)t+Lt=yx(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,mn)/(mn)T=lcm(L,m-n)/(m-n),再将tt放到[0,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 博客中。