【算法笔记】扩展 Euclid 算法

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

前言

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

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

题目引入

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

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

样例输入 \(2\)\(a=2,b=-4,c=1\)
样例输出 \(2\):无解,因为 \(2x-4y\) 永远是偶数,但 \(c=1\),是奇数。

最大公约数及 Euclid 算法

最大公约数的概念及性质

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

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

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

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

  • 交换律:\(\gcd(a,b)=\gcd(b,a)\),特别地,\(\gcd(a,0)=\gcd(0,a)=a\)
  • 可以提出常数:\(\gcd(ka,kb)=k\gcd(a,b)\ (k\in\mathbb{Z}\));
  • 对加减法及取模自由:\(\gcd(a,b)=\gcd(a,b\pm a)=\gcd(a,b\pm ka)=\gcd(a,b \bmod a)\ (k\in\mathbb{Z})\)

Euclid 算法的内容

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

答曰:我会 \(\mathcal{O}(n)\) 暴力枚举!

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

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

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

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

  • \(n=897\ne0\),故我们令 \(m=897\)\(n=1081\bmod897=184\);原问题即转化为求 \(\gcd(897,184)\)
  • \(n=184\ne0\),故我们令 \(m=184\)\(n=897\bmod184=161\);原问题即转化为求 \(\gcd(184,161)\)
  • \(n=161\ne0\),故我们令 \(m=161\)\(n=184\bmod161=23\);原问题即转化为求 \(\gcd(161,23)\)
  • \(n=23\ne0\),故我们令 \(m=23\)\(n=161\bmod23=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\}\),其递推式为:

\[U_i=\begin{cases} m & i=0,\\ n & i=1,\\ U_{i-2}\bmod U_{i-1} & \text{otherwise.} \end{cases} \]

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

\[F_i=\begin{cases} 1 & i\le 1, \\ F_{i-1}+F_{i-2} & i>2 \end{cases}\ (i=0,1,\cdots) \]

显然数列 \(\{U\}\) 中存在一项 \(U_x=0\)

故有 \(F_0=U_x=0,F_1\ge U_x\)

又因为 \(U_i \bmod U_{i+1}=U_{i+2}\),所以 \(U_{i}\ge U_{i+1}+U_{i+2}\)

利用数学归纳法可得:\(U_i\ge F_{x-i+1}\),故 \(n=U_1\ge F_{x}\)

注意到 \(F_x>\dfrac{(\phi+1)^x}{\sqrt{5}}\),其中 \(\phi=\dfrac{\sqrt{5}-1}{2}\),所以 \(x<\log_{(\phi+1)}(\sqrt{5}n)\)

故 Euclid 算法迭代的次数不超过 \(\log_{(\phi+1)}(\sqrt{5}n)\) 次,时间复杂度为 \(\mathcal{O}(\log n)\)\(\Box\)

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

不定方程概念的引入

不定方程

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

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

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

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

针对这个问题,早在 \(17\) 世纪,法国数学家 \(\mathcal{B\acute{e}zout}\) 就给出了结论:

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

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

\(S\) 是最小的能用 \(ax+by\) 表示的正整数,则有
\(a\bmod S=a-S\left \lfloor \dfrac{a}{S} \right \rfloor=a-(ax+by)\left \lfloor \dfrac{a}{S} \right \rfloor=a \left(1-\left \lfloor \dfrac{a}{S} \right \rfloor \right)-b\left(\left \lfloor \dfrac{a}{S} \right \rfloor\right)\le S\)

又因为 \(S\) 是最小的能用 \(ax+by\) 表示的整数,所以 \(a\bmod S=0\),即 \(S\mid a\)

同理 \(S\mid b\)。即 \(S\)\(a,b\) 的公约数,由最大公约数定义得 \(S\le \gcd(a,b)\)

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

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

且对于任意 \(c=kS\ (k\in\mathbb{Z})\),都存在 \(x'=kx,y'=ky\),使得 \(ax'+by'=c\),即 \(ax+by=c\) 存在正整数解。

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

假设 \(x_0,y_0\)\(ax+by=\gcd(a,b)\) 的整数解,则任意的 \(ax+by=c\) 的整数解 \(x,y\),都有 \(x=\dfrac{c}{\gcd(a,b)}x_0,y=\dfrac{c}{\gcd(a,b)}y_0\)

\(\gcd(a,b)\nmid c\),则 \(x,y\notin\mathbb{Z}\),与 \(x,y\) 为整数矛盾。

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

Bézout 定理的推广

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

关于 \(x_1,x_2,\cdots,x_n\)\(n\) 元一次不定方程 \(\sum_{i=1}^{n}a_ix_i=b\) 有整数解,当且仅当 \(\gcd_{i=1}^{n}(a_i)\mid b\)。证明留给读者作为练习。

不定方程的解

扩展 Euclid 算法的引入

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

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

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

扩展 Euclid 算法的内容

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

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

重新合并,得到了 \(a\left(\left \lfloor \dfrac{b}{a} \right \rfloor y+x\right)+(b\bmod a)y=\gcd(a,b)\)

右边由 \(\gcd\) 的性质有 \(\gcd(a,b)=\gcd(a,a\bmod b)\),我们令左边 \(x'=y,y'=\left \lfloor \dfrac{b}{a} \right \rfloor y+x\),则有 \((a\bmod b)x'+ay'=\gcd(a,a\bmod b)\),此时,我们就得到了一个新的,系数范围更小的不定方程。

如果我们求出了方程中的 \(x',y'\),就可以代入回去,得到 \(y=x',x=y'-\left \lfloor \dfrac{b}{a} \right \rfloor x'\)。整个算法就可以递归地进行了。

递归的出口为 \(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)\mid 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 算法步骤基本相同,时间复杂度依旧为 \(\mathcal{O}(\log n)\)

扩展 Euclid 算法的应用

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

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

例题

本题目列表会持续更新

posted @ 2022-01-27 20:49  CaO氧化钙  阅读(446)  评论(0)    收藏  举报