【算法笔记】扩展 Euclid 算法
- 本文总计约 8000 字,阅读大约需要 30 分钟。
- 警告!警告!警告!本文有大量的 公式渲染,可能会导致加载异常缓慢!
前言
扩展 Euclid 算法是数论中最经典,也是最简单(?)的算法了,但是它的应用又十分的广泛。所以介绍它是十分有必要的。
数论的知识真的是非常奇怪呢(bushi),因为它们感性地理解起来非常轻松,但是要严谨证明就很难 QAQ,笔者在文中加入了大量的数学证明,尽管可能有读者不爱看因为其实我也不爱看,但是基于数学严谨的思想,我还是把它们加了进来,这可能会导致浏览器渲染起来蛮麻烦的,不过还是请读者多多包涵了。
题目引入
给定三个整数 ,求关于 的二元一次不定方程 的一组整数解,注意该方程可能没有整数解。
样例输入 :;
样例输出 :。注意可能有其它的满足要求的解,如 。
样例输入 :;
样例输出 :无解,因为 永远是偶数,但 ,是奇数。
最大公约数及 Euclid 算法
最大公约数的概念及性质
尽管最大公约数在我们小学的时候就已经接触了,但我们还是需要重新介绍一下它:
如果对于两个整数 ,若 是最大的既可以整除 (记作 ),又可以整除 的正整数,即 是 最大的公约数,那么我们称 为 的最大公约数(好像很废话的概念 QwQ),记作 。
特别地,若两个整数 满足 ,则我们称 与 互质,记作 。
显然 函数有如下的性质,此处不加以证明:
- 交换律:,特别地,;
- 可以提出常数:);
- 对加减法及取模自由:。
Euclid 算法的内容
现在我们已经知道了最大公约数的基本概念了,那么我们怎么求任意两个给定的整数 的最大公约数呢?
答曰:我会 暴力枚举!
然而,假如我们令 甚至是 ,那么 的枚举是一定会超时的 QwQ,我们就需要一种更快的算法来求出上述问题中的 。于是,Euclid 算法应运而生。
Euclid 算法,又称辗转相除法,顾名思义,是由古希腊数学家 发明的算法,其步骤如下:
- 对于两个整数 ,如果有 ,那么我们求出 的值;
- 接下来,使 ;
- 重复上述两个步骤,直到 时, 的值就是我们所求的最大公约数。
例如,我们要求 ,步骤如下:
- ,故我们令 ,;原问题即转化为求 ;
- ,故我们令 ,;原问题即转化为求 ;
- ,故我们令 ,;原问题即转化为求 ;
- ,故我们令 ,;此时 ,所以 。
像这样,不断地用一个数去除以另一个数,辗转不停,故得名为辗转相除法。
代码实现
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。
时间复杂度分析
(前方数学证明警告!)
有一种非常经典的证明其时间复杂度的方法,如下:
我们构造数列 ,其递推式为:
定义 Fibonacci 数列 递推式为:
显然数列 中存在一项 。
故有 。
又因为 ,所以 。
利用数学归纳法可得:,故 。
注意到 ,其中 ,所以 。
故 Euclid 算法迭代的次数不超过 次,时间复杂度为 。
这就是说,Euclid 算法相比较于 的暴力枚举,有了很大程度的优化。
不定方程概念的引入
不定方程
一个含有多个未知数的整系数方程称为不定方程,又称 Diophantine 方程。即,形如: 的方程即为不定方程,其中 均为整数()。如果这个方程只有两个未知数,且未知数的次数均为 ,那么我们称其为二元一次不定方程。
注意,本文在讨论不定方程时,默认指的是二元一次不定方程,即形如 的方程。
不定方程有解问题 — — Bézout 定理
正如题目引入中所说的,二元一次不定方程是不一定有解的,例如 就没有整数解。所以,我们应该如何判断,一个不定方程是否有解呢?
针对这个问题,早在 世纪,法国数学家 就给出了结论:
关于 的不定方程 有整数解,当且仅当 。或者用另一种表述: 一定有整数解。这就是著名的 Bézout 定理,或者国内又译为裴蜀定理或贝祖定理。证明如下:(数学证明警告!)
先证充分性,即若 ,则 有整数解。
设 是最小的能用 表示的正整数,则有
。
又因为 是最小的能用 表示的正整数,所以 ,即 ;
同理 。即 是 的公约数,由最大公约数定义得 。
又因为 ,所以 ,故 。
综上 ,即 有整数解。
且对于任意 ,都存在 ,使得 ,即 存在正整数解。
再证必要性,即若 有整数解,则 。
假设 是 的整数解,则任意的 的整数解 ,都有 。
若 ,则 ,与 为整数矛盾。
综上所述,不定方程 有整数解当且仅当 。
Bézout 定理的推广
当然,即使 Bézout 定理是关于二元一次不定方程的,但我们依旧可以将其推广到 元的不定方程:
关于 的 元一次不定方程 有整数解,当且仅当 。证明留给读者作为练习。
不定方程的解
扩展 Euclid 算法的引入
对于不定方程 ,我们可以通过 Euclid 算法求出 的值,然后用 Bézout 定理判断其是否存在整数解。
但是,我们只是知道了一个不定方程的可解性,又如何求它的具体解呢?
其实,这个问题通过对 Euclid 算法进行一些小小的拓展、变形,就可以求出来。也就是接下来的扩展 Euclid 算法。
扩展 Euclid 算法的内容
我们现在考虑最简单的一种情况:,应该如何求其整数解呢?
首先,小学的数学知识就告诉我们,商乘除数加余数等于被除数,这一点是永远成立的,所以我们用 除以 ,有 。代入原方程,有 。
重新合并,得到了 。
右边由 的性质有 ,我们令左边 ,则有 ,此时,我们就得到了一个新的,系数范围更小的不定方程。
如果我们求出了方程中的 ,就可以代入回去,得到 。整个算法就可以递归地进行了。
递归的出口为 ,此时我们有 。
像这样,我们利用了系数与系数之间 “辗转相除” 的方式,求出了 的一组解,同时,我们也可以求出 的值,即递归出口处 的值。
这种算法是通过 Euclid 算法变形而来,所以我们称其为扩展 Euclid 算法,简称扩欧。
拓展情形
通过扩欧算法,我们可以求出 的一组解。但是,根据 Bézout 定理,对于任意的 ,
代码
#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 算法步骤基本相同,时间复杂度依旧为 。
扩展 Euclid 算法的应用
扩欧可以用于同余方程的求解:形如 的方程称为同余方程。我们通过余数的性质,可以将其改写为不定方程的形式:。移项,得 ,其中 是代求的未知数,我们可以通过扩欧求出来 ,也就是可以求出同余方程的解了。这种情况下,我们就可以将若干个同余方程连接成同余方程组,并求出满足同余方程组的解— —这就是著名的“中国剩余定理”(CRT)。
同时,我们可以依靠乘法逆元,在取模意义下完成除法,所谓乘法逆元,指的就是使得给定的 满足 的 的值。我们将其可以变形为 的形式,于是我们就可以通过扩欧求出 ,也就是求出 模 意义下的乘法逆元。
例题
本题目列表会持续更新。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】