【算法笔记】扩展 Euclid 算法

  • 本文总计约 8000 字,阅读大约需要 30 分钟
  • 警告!警告!警告!本文有大量的 LATEX 公式渲染,可能会导致加载异常缓慢!

前言

扩展 Euclid 算法是数论中最经典,也是最简单(?)的算法了,但是它的应用又十分的广泛。所以介绍它是十分有必要的。

数论的知识真的是非常奇怪呢(bushi),因为它们感性地理解起来非常轻松,但是要严谨证明就很难 QAQ,笔者在文中加入了大量的数学证明,尽管可能有读者不爱看因为其实我也不爱看,但是基于数学严谨的思想,我还是把它们加了进来,这可能会导致浏览器渲染起来蛮麻烦的,不过还是请读者多多包涵了。

题目引入

给定三个整数 a,b,c,求关于 x,y 的二元一次不定方程 ax+by=c 的一组整数解,注意该方程可能没有整数解。

样例输入 1a=3,b=2,c=7
样例输出 1x=1,y=2。注意可能有其它的满足要求的解,如 x=3,y=1

样例输入 2a=2,b=4,c=1
样例输出 2:无解,因为 2x4y 永远是偶数,但 c=1,是奇数。

最大公约数及 Euclid 算法

最大公约数的概念及性质

尽管最大公约数在我们小学的时候就已经接触了,但我们还是需要重新介绍一下它:

如果对于两个整数 a,b,若 m 是最大的既可以整除 a(记作 ma),又可以整除 b 的正整数,即 ma,b 最大的公约数,那么我们称 ma,b最大公约数(好像很废话的概念 QwQ),记作 m=gcd(a,b)

特别地,若两个整数 a,b 满足 gcd(a,b)=1,则我们称 ab 互质,记作 ab

显然 gcd 函数有如下的性质,此处不加以证明:

  • 交换律:gcd(a,b)=gcd(b,a),特别地,gcd(a,0)=gcd(0,a)=a
  • 可以提出常数:gcd(ka,kb)=kgcd(a,b) (kZ);
  • 对加减法及取模自由:gcd(a,b)=gcd(a,b±a)=gcd(a,b±ka)=gcd(a,bmoda) (kZ)

Euclid 算法的内容

现在我们已经知道了最大公约数的基本概念了,那么我们怎么求任意两个给定的整数 m,n(mn) 的最大公约数呢?

答曰:我会 O(n) 暴力枚举!

然而,假如我们令 n109 甚至是 n1018,那么 O(n) 的枚举是一定会超时的 QwQ,我们就需要一种更快的算法来求出上述问题中的 gcd(m,n)。于是,Euclid 算法应运而生。

Euclid 算法,又称辗转相除法,顾名思义,是由古希腊数学家 Euclid 发明的算法,其步骤如下:

  • 对于两个整数 m,n,如果有 n0,那么我们求出 r=mmodn 的值;
  • 接下来,使 m=n,n=r
  • 重复上述两个步骤,直到 n=0 时,m 的值就是我们所求的最大公约数。

例如,我们要求 gcd(1081,897),步骤如下:

  • n=8970,故我们令 m=897n=1081mod897=184;原问题即转化为求 gcd(897,184)
  • n=1840,故我们令 m=184n=897mod184=161;原问题即转化为求 gcd(184,161)
  • n=1610,故我们令 m=161n=184mod161=23;原问题即转化为求 gcd(161,23)
  • n=230,故我们令 m=23n=161mod23=0;此时 n=0,所以 gcd(1081,897)=m=23

像这样,不断地用一个数去除以另一个数,辗转不停,故得名为辗转相除法

代码实现

Euclid 算法的代码非常好写,这里给出迭代求最大公约数的代码:

#include <cstdio>
#include <algorithm>  //有用于求 gcd 的标准函数 __gcd(int, int)
using namespace std;

inline int Euclid(int x, int y) {  //辗转相除法求 gcd(x, y),inline 防止无效信息入栈
	int r;
	
	while(y) {
		r = x % y;
		x = y;
		y = r;
	}
	
	return x;
}

long long m, n;

int main(void) {
	scanf("%lld%lld", &m, &n);
	printf("%lld", Euclid(m, n));
	return 0;
}
//by CaO

当然,C++<algorithm> 库里有 __gcd(int, int) 函数,功能与上述函数相同,但我不知道 NOIP 中让不让用 QwQ

时间复杂度分析

前方数学证明警告!
有一种非常经典的证明其时间复杂度的方法,如下:

我们构造数列 {U},其递推式为:

Ui={mi=0,ni=1,Ui2modUi1otherwise.

定义 Fibonacci 数列 {F} 递推式为:

Fi={1i1,Fi1+Fi2i>2 (i=0,1,)

显然数列 {U} 中存在一项 Ux=0

故有 F0=Ux=0,F1Ux

又因为 UimodUi+1=Ui+2,所以 UiUi+1+Ui+2

利用数学归纳法可得:UiFxi+1,故 n=U1Fx

注意到 Fx>(ϕ+1)x5,其中 ϕ=512,所以 x<log(ϕ+1)(5n)

故 Euclid 算法迭代的次数不超过 log(ϕ+1)(5n) 次,时间复杂度为 O(logn)

这就是说,Euclid 算法相比较于 O(n) 的暴力枚举,有了很大程度的优化。

不定方程概念的引入

不定方程

一个含有多个未知数的整系数方程称为不定方程,又称 Diophantine 方程。即,形如:i=1naixipi=b 的方程即为不定方程,其中 ai,pi,b 均为整数(i=1,2,,n)。如果这个方程只有两个未知数,且未知数的次数均为 1,那么我们称其为二元一次不定方程

注意,本文在讨论不定方程时,默认指的是二元一次不定方程,即形如 ax+by=c 的方程。

不定方程有解问题 — — Bézout 定理

正如题目引入中所说的,二元一次不定方程是不一定有解的,例如 2x4y=1 就没有整数解。所以,我们应该如何判断,一个不定方程是否有解呢?

针对这个问题,早在 17 世纪,法国数学家 Be´zout 就给出了结论:

关于 x,y 的不定方程 ax+by=c 有整数解,当且仅当 gcd(a,b)c。或者用另一种表述:ax+by=gcd(a,b) 一定有整数解。这就是著名的 Bézout 定理,或者国内又译为裴蜀定理或贝祖定理。证明如下:(数学证明警告!

先证充分性,即若 gcd(a,b)c,则 ax+by=c 有整数解。

S 是最小的能用 ax+by 表示的正整数,则有
amodS=aSaS=a(ax+by)aS=a(1aS)b(aS)S

又因为 S 是最小的能用 ax+by 表示的整数,所以 amodS=0,即 Sa

同理 Sb。即 Sa,b 的公约数,由最大公约数定义得 Sgcd(a,b)

又因为 gcd(a,b)(ax+by),所以 gcd(a,b)S,故 gcd(a,b)S

综上 S=gcd(a,b),即 ax+by=gcd(a,b) 有整数解。

且对于任意 c=kS (kZ),都存在 x=kx,y=ky,使得 ax+by=c,即 ax+by=c 存在正整数解。

再证必要性,即若 ax+by=c 有整数解,则 gcd(a,b)c

假设 x0,y0ax+by=gcd(a,b) 的整数解,则任意的 ax+by=c 的整数解 x,y,都有 x=cgcd(a,b)x0,y=cgcd(a,b)y0

gcd(a,b)c,则 x,yZ,与 x,y 为整数矛盾。

综上所述,不定方程 ax+by=c 有整数解当且仅当 gcd(a,b)c

Bézout 定理的推广

当然,即使 Bézout 定理是关于二元一次不定方程的,但我们依旧可以将其推广到 n 元的不定方程:

关于 x1,x2,,xnn 元一次不定方程 i=1naixi=b 有整数解,当且仅当 gcdi=1n(ai)b。证明留给读者作为练习。

不定方程的解

扩展 Euclid 算法的引入

对于不定方程 ax+by=c,我们可以通过 Euclid 算法求出 gcd(a,b) 的值,然后用 Bézout 定理判断其是否存在整数解。

但是,我们只是知道了一个不定方程的可解性,又如何求它的具体解呢?

其实,这个问题通过对 Euclid 算法进行一些小小的拓展、变形,就可以求出来。也就是接下来的扩展 Euclid 算法。

扩展 Euclid 算法的内容

我们现在考虑最简单的一种情况:ax+by=gcd(a,b),应该如何求其整数解呢?

首先,小学的数学知识就告诉我们,商乘除数加余数等于被除数,这一点是永远成立的,所以我们用 b 除以 a,有 b=aba+(bmoda)。代入原方程,有 ax+(aba+(bmoda))y=gcd(a,b)

重新合并,得到了 a(bay+x)+(bmoda)y=gcd(a,b)

右边由 gcd 的性质有 gcd(a,b)=gcd(a,amodb),我们令左边 x=y,y=bay+x,则有 (amodb)x+ay=gcd(a,amodb),此时,我们就得到了一个新的,系数范围更小的不定方程。

如果我们求出了方程中的 x,y,就可以代入回去,得到 y=x,x=ybax。整个算法就可以递归地进行了。

递归的出口为 b=0,此时我们有 x=1,y=0

像这样,我们利用了系数与系数之间 “辗转相除” 的方式,求出了 ax+by=gcd(a,b) 的一组解,同时,我们也可以求出 gcd(a,b) 的值,即递归出口处 a 的值。

这种算法是通过 Euclid 算法变形而来,所以我们称其为扩展 Euclid 算法,简称扩欧

拓展情形

通过扩欧算法,我们可以求出 ax+by=gcd(a,b) 的一组解。但是,根据 Bézout 定理,对于任意的 gcd(a,b)c

代码

#include <cstdio>
using namespace std;

int a, b, c, k, x, y;
int exgcd(int a, int b, int& x, int& y) {  //扩欧求不定方程
	if(b == 0) {
		x = 1;
		y = 0;
		return a;  //递归出口
	}
	
	int ans = (b, a % b, y, x);
	y -= (a / b * x);  //状态转移
	return ans;
}

int main(void) {
	scanf("%d%d%d", &a, &b, &c);
	
	if(c % exgcd(a, b, x, y) != 0) {  //根据裴蜀定理,此时方程无整数解
		printf("No Solution.\n");
	}
	
	else {
		k = c / exgcd(a, b, x, y);
		printf("x=%d, y=%d\n", k * x, k * y);
	}
	
	return 0;
}
//by CaO

时间复杂度分析

扩欧的算法与 Euclid 算法步骤基本相同,时间复杂度依旧为 O(logn)

扩展 Euclid 算法的应用

扩欧可以用于同余方程的求解:形如 xa(modm) 的方程称为同余方程。我们通过余数的性质,可以将其改写为不定方程的形式:x=km+a。移项,得 xkm=a,其中 x,k 是代求的未知数,我们可以通过扩欧求出来 x,也就是可以求出同余方程的解了。这种情况下,我们就可以将若干个同余方程连接成同余方程组,并求出满足同余方程组的解— —这就是著名的“中国剩余定理”(CRT)。

同时,我们可以依靠乘法逆元,在取模意义下完成除法,所谓乘法逆元,指的就是使得给定的 a,m 满足 ax1(modm)x 的值。我们将其可以变形为 ax+km=1 的形式,于是我们就可以通过扩欧求出 x,也就是求出 am 意义下的乘法逆元。

例题

本题目列表会持续更新

posted @   CaO氧化钙  阅读(299)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示