# 最大公约数
若自然数
d
,同时是自然数a
和b
的约数,我们称d
为a
和b
的公约数。而所有公约数中最大的,我们称为a
和b
的最大公约数,记为gcd(a,b)
。
求最大公约数的算法有很多,比如:分解质因数、九章算术里的更相减损术、短除法等等。但是应用最广泛的还是今天的主角 -- 辗转相除法。
# 辗转相除法
古希腊数学家欧几里得在其著作《The Elements》中最早描述了这种算法,所以又被命名为欧几里得算法。
辗转相除法依赖除法的一个基本性质(下面称为除法性质):
假设
d|a
,同时d|b
,那么一定有d|(ax+by)
。
其中 |
表示整除的意思, d|a
,表示 d
可以整除 a
。既, a
是 d
的倍数,比如 3
可以整除 9
,表示为 3|9
。
所以,这个性质的意思是,如果 d 能同时整除 a 和 b,那么 d 也能整除 a 的倍数加上 b 的倍数。
例如: 4
能整除 8
,也能整除 12
,那么 4
也能整除 8*3+12*5
(其中 3 和 5 可以取任意整数)。
通过这个性质,我们就能得到辗转相除法的最终公式: gcd(a, b)=gcd(b, a mod b)
。
# 证明推导
当
a<b
的时候,有a%b==a
,那么最终公式自然成立。当
a>=b
的时候。首先看模数部分,先把模运算改成普通的四则运算: (其中 用来表示 a 除以 b 并向下取整)。
举例: 。在上面的公式中,因为 一定是一个整数,我们用
x
来表示这个整数,那么,公式变成 。把这个公式带入到最终结果中得到: 。
如何证明两面是相等的呢?
- 先看左面,假设
d
为a
和b
的任意公约数,那么显然d
可以整除a
和b
,再根据上面的除法性质,我们知道d
也能整除a-x*b
(其中a
的整数倍是1
,b
的整数倍是-x
)。又因为d
是任意公约数,那么d
也可以是最大公约数,所以gcd(a,b)=d,gcd(b,a-x*b)=d
,推出gcd(a,b)==>gcd(b, a-x*b)
。 - 再看右面,假设
d
为b
和a-x*b
的任意公约数,那么显然d
可以整除b
,那么根据上面的除法性质,我们知道d
也能整除a-x*b+x*b
(其中a-x*b
的整数倍是1
,b
的整数倍是x
),也就是d
能整除a
。同步骤 1 的推论,可以得出gcd(b, a-x*b)==>gcd(a,b)
。 - 综合步骤 1 和步骤 2,得出最终结论
gcd(a, b)=gcd(b, a mod b)
。
- 先看左面,假设
# Code
执行到最后的最终形态一定是 gcd (a, 0)。既,b=0 导致无法再进行除法运算,此时,最大公约数就是 a,同时这个 a 也是最后一个值不为 0 的 b。
int gcd(int a, int b) { | |
return b ? gcd(b, a % b) : a; | |
} |
# 扩展欧几里得算法
裴蜀定理:关于任意的正整数 a,b,一定有非零数对
x,y
,可以让ax+ay=gcd(a, b)
。
扩展欧几里得算法的作用就是求出其中的系数 x,y
。
# 证明推导
在上一节,我们已经知道了欧几里得算法的最终形态:
gcd(a, b)=gcd(b, a mod b)
。根据裴蜀定理,现在有公式
ax+by=gcd(a, b)
。联立两个公式可以推出:ax+by=gcd(b, a mod b)
。根据裴蜀定理,从
gcd(b, a mod b)
又可以推出。代入 到上面的公式,并且联合最初的,从而得到。
我们看一下其中 与 的关系。将上一步的公式展开:
由此,我们得到了 与 的关系:其中 原封不动的变成了 ,而 变成了 。该规则说明最终的,一定能通过子问题 构造出来。
那么最初的 是什么呢?我们注意到在欧几里得算法中,辗转相除到最后的子问题一定是
gcd(a, 0)
,那么对于等式 ,如果让 一定成立。且由于b
此时是 0,所以 可以是任意整数,为了方便我们就让它也是 0。
# Code
输入示例:
2
4 6
8 18
输出示例:
-1 1
-2 1
在代码实现上,在辗转相除计算最大公约数的基础上,同时把系数 x,y
也求出来。其中有两点需要注意一下:
x,y
必须传递引用,否则在递归的过程中,x,y
都不相同的。如exgcd(6,4)
中的x,y
与exgcd(4,2)
中的x,y
与exgcd(2,0)
中初始化出来的x=1,y=0
都是不相同的,而我们最后要得到初始化的那个x=1,y=0
中的x,y
,所以在传参的时候要传引用。- 因为求公约数是自顶向上的递归,而求系数
x,y
的时候是从1,0
求到x,y
,因此必须使用自底向上的递归,也就是把求 y 的部分放到求公约数之后。
#include <iostream> | |
using namespace std; | |
int exgcd(int a, int b, int &x, int &y) { | |
if (!b) { | |
x = 1, y = 0; // 初始化 x,y | |
return a; | |
} | |
//x 和 y 也要换位置,因为 a | |
int d = exgcd(b, a % b, y, x); | |
y -= a / b * x; // 自底向上的递归求 y。 | |
return d; | |
} | |
int main() { | |
int n; | |
cin >> n; | |
while ( n -- ) { | |
int a, b, x, y; | |
cin >> a >> b; | |
exgcd(a, b, x, y); | |
cout << x << ' ' << y << endl; | |
} | |
} |
对于没办法直接传递引用的语言 (“对,说的就是你,python”),我们需要手动翻转 x 和 y 的值。
def exgcd(a, b): | |
if b == 0: | |
return a, 1, 0 | |
else: | |
d, x, y = exgcd(b, a % b) | |
# 此处手动反转 x,y | |
x, y = y, x - (a // b) * y | |
return d, x, y |