[Python] 体验用欧几里得算法计算最大公约数的过程

80 阅读1分钟

背景

我们在小学时,学过最大公约数(Greatest Common Divisor\text{Greatest Common Divisor})的知识,但是当时并没有学习比较高效的求最大公约数的方法。使用欧几里得算法(Euclidean Algorithm\text{Euclidean Algorithm}),可以高效地计算出自然数的最大公约数(两个自然数不能同时为 00)。本文带您体验这一过程。

正文

根据定义暴力计算

根据最大公约数的定义,我们可以用暴力的方式进行计算自然数 aa 和自然数 bb 的最大公约数(a=0a=0b=0b=0 不能同时成立)。对应的 Python\text{Python} 代码如下

def brute_force_gcd(a, b):
    if a == 0 and b == 0:
        raise ValueError("a and b cannot be both 0")
    if a == 0:
        return b
    if b == 0:
        return a
    candidate = min(a, b)
    while True:
        if a % candidate == 0 and b % candidate == 0:
            return candidate
        candidate -= 1

由于所有的自然数都能被 11 整除,所以上述代码求出的最大公约数 gcd1gcd\ge 1 总是成立。

观察

如果固定 bb 的值,让 aa 变化(也可以固定 aa,让 bb 变化,两种方式没有本质区别),我们看一看求出的结果是否有特殊的规律。

b=1b=1

b=1b=1 时,对任意自然数 aagcd(a,1)=1gcd(a,1)=1 显然成立。

b=2b=2

b=2b=2 时,我们可以列举比较小的 aa 值所对应的计算结果

image.png

看起来 gcd(a,2)gcd(a,2) 的值是在 1122 之间交替出现。解释如下 ⬇️

  • aa 是偶数时,222\mid 2 并且 2a2\mid a,所以 gcd(a,2)=2gcd(a,2)=2
  • aa 是奇数时
    • 因为 2a2\nmid a,所以 gcd(a,2)2gcd(a,2)\ne 2
    • 那么 gcd(a,2)=1gcd(a,2)=1

b=3b=3

b=3b=3 时,我们可以列举比较小的 aa 值所对应的计算结果

image.png

看起来 gcd(a,3)gcd(a,3) 的值会以 3,1,13,1,1 这样周期反复出现。解释如下 ⬇️

  • a0(mod3)a\equiv 0 \pmod 3 时,333|3 并且 3a3|a,所以 gcd(a,3)=3gcd(a,3)=3
  • a1(mod3)a\equiv 1 \pmod 3
    • 3a3\nmid a,所以 gcd(a,3)3gcd(a,3)\ne 3
    • 232\nmid 3,所以 gcd(a,3)2gcd(a,3)\ne 2
    • 那么 gcd(a,3)=1gcd(a,3)=1
  • a2(mod3)a\equiv 2 \pmod 3
    • 3a3\nmid a,所以 gcd(a,3)3gcd(a,3)\ne 3
    • 232\nmid 3,所以 gcd(a,3)2gcd(a,3)\ne 2
    • 那么 gcd(a,3)=1gcd(a,3)=1

b=4b=4

b=4b=4 时,我们可以列举比较小的 aa 值所对应的计算结果

image.png

看起来 gcd(a,4)gcd(a,4) 的值会以 4,1,2,14,1,2,1 这样周期反复出现。解释如下 ⬇️

  • a0(mod4)a\equiv 0 \pmod 4 时,444|4 并且 4a4|a,所以 gcd(a,4)=4gcd(a,4)=4
  • a1(mod4)a\equiv 1 \pmod 4
    • 4a4\nmid a,所以 gcd(a,4)4gcd(a,4)\ne 4
    • 343\nmid 4,所以 gcd(a,4)3gcd(a,4)\ne 3
    • 2a2\nmid a,所以 gcd(a,4)2gcd(a,4)\ne 2
    • 那么 gcd(a,3)=1gcd(a,3)=1
  • a2(mod3)a\equiv 2 \pmod 3
    • 4a4\nmid a,所以 gcd(a,4)4gcd(a,4)\ne 4
    • 343\nmid 4,所以 gcd(a,4)3gcd(a,4)\ne 3
    • 242\mid 4,并且 2a2\mid a,所以 gcd(a,4)=2gcd(a,4)= 2
  • a3(mod4)a\equiv 3 \pmod 4
    • 4a4\nmid a,所以 gcd(a,4)4gcd(a,4)\ne 4
    • 343\nmid 4,所以 gcd(a,4)3gcd(a,4)\ne 3
    • 2a2\nmid a,所以 gcd(a,4)2gcd(a,4)\ne 2
    • 那么 gcd(a,4)=1gcd(a,4)=1

一般的情形

分析了以上几个值比较小的 bb 之后,我们尝试从中找规律。假设 aba\ge b 成立,看起来以下等式是成立的

gcd(a,b)=gcd(ab,b)gcd(a,b)=gcd(a-b,b)

我们看看能否证明它。为了方便描述,我们记 gcd(a,b)=ggcd(a,b)=g, gcd(ab,b)=ggcd(a-b,b)=g'

我们的目标是证明 g=gg=g' (或者找到两者不相等的反例)。

gcd(a,b)=ggcd(a,b)=g,根据最大公约数的定义,以下两者成立

  • gag\mid a
  • gbg\mid b

所以 g(ab)g\mid(a-b)gg 既是 aba-b 的约数,又是 bb 的约数,那么 ggaba-bbb公约数。按照最大公约数的定义,ggg\le g' 成立(因为 gg'aba-bbb最大公约数)。

反过来看,gcd(ab,b)=ggcd(a-b,b)=g',根据最大公约数的定义,以下两者成立

  • g(ab)g'\mid (a-b)
  • gbg' \mid b

所以 g((ab)+b)g' \mid ((a-b)+b),即 gag' \mid agg' 既是 bb 的约数,又是 aa 的约数,那么 gg'aabb公约数。按照最大公约数的定义,ggg'\le g 成立(因为 ggaabb最大公约数)。

既然 ggg\le g'ggg'\le g 同时成立,那么 g=gg=g' 成立。也就是说,对正整数 a,ba,b 而言,当 aba\ge b 成立时,以下等式总是成立。

gcd(a,b)=gcd(ab,b)gcd(a,b)=gcd(a-b,b)

在此基础上,可以推出 ⬇️ (a,ba,b 都是正整数)

gcd(a,b)=gcd(ab,b)gcd(a,b)=gcd(a-b,b)
=gcd(a2b,b)=gcd(a-2b,b)
=gcd(a3b,b)=gcd(a-3b,b)
\cdots
=gcd(amodb,b)=gcd(a \bmod b,b)

所以对任意正整数 a,ba,b 而言,以下等式成立

gcd(a,b)=gcd(amodb,b)gcd(a,b)=gcd(a \bmod b,b)

这就是欧几里得算法 (Euclidean Algorithm\text{Euclidean Algorithm}) 的核心思想。

Python\text{Python} 程序实现欧几里得算法

55553434 为例,我们利用欧几里得算法来计算两者的最大公约数,具体过程如下 ⬇️

gcd(55,34)=gcd(55mod34,34)gcd(55,34)=gcd(55 \bmod 34, 34)
=gcd(21,34)=gcd(34,21)=gcd(21, 34)=gcd(34, 21)
=gcd(34mod21,21)=gcd(34 \bmod 21, 21)
=gcd(13,21)=gcd(21,13)=gcd(13, 21)=gcd(21, 13)
=gcd(21mod13,13)=gcd(21 \bmod 13, 13)
=gcd(8,13)=gcd(13,8)=gcd(8, 13)=gcd(13,8)
=gcd(13mod8,8)=gcd(13 \bmod 8, 8)
=gcd(5,8)=gcd(8,5)=gcd(5, 8)=gcd(8, 5)
=gcd(8mod5,5)=gcd(8 \bmod 5, 5)
=gcd(3,5)=gcd(5,3)=gcd(3, 5)=gcd(5, 3)
=gcd(5mod3,3)=gcd(5 \bmod 3, 3)
=gcd(2,3)=gcd(3,2)=gcd(2, 3)=gcd(3,2)
=gcd(3mod2,2)=gcd(3 \bmod 2, 2)
=gcd(1,2)=gcd(2,1)=gcd(1, 2)=gcd(2,1)
=gcd(2mod1,1)=gcd(2 \bmod 1, 1)
=gcd(0,1)=gcd(0, 1)
=1=1

用代码来实现,可以这样写 ⬇️

def gcd(a, b):
    if (a, b) == (0, 0):
        raise ValueError("a and b cannot be both 0")
    if b == 0:
        return a
    return gcd(b, a % b)

当我们用这段代码计算 gcd(55,34)gcd(55,34) 时,函数的调用会形成一个栈。我想用比较直观的方式展示这个函数调用栈,于是在 trae 的帮助下,将其改造成了如下的代码(我觉得不必关心 show_stack 方法的细节,知道它可以展示函数调用栈的内容就够了,show_stack 方法的细节我自己也不懂 😂)

import inspect

def gcd(a, b):
    if (a, b) == (0, 0):
        raise ValueError("a and b cannot be both 0")
    if b == 0:
        show_stack()
        return a
    return gcd(b, a % b)

def show_stack():
    # show_stack 方法的原始内容由 trae 提供,我做了小调整
    for frame_info in inspect.stack()[:][1:]:
        frame = frame_info.frame
        args = []
        for name in frame.f_code.co_varnames[:frame.f_code.co_argcount]:
            if name in frame.f_locals:
                args.append(f"{name}={frame.f_locals[name]}")
        print(f"{frame_info.function}({', '.join(args)})")

if __name__ == "__main__":
    gcd(55, 34)

请将上述代码保存为 calc_gcd.py,使用如下命令可以运行 calc_gcd.py

python3 calc_gcd.py

运行结果如下

gcd(a=1, b=0)
gcd(a=2, b=1)
gcd(a=3, b=2)
gcd(a=5, b=3)
gcd(a=8, b=5)
gcd(a=13, b=8)
gcd(a=21, b=13)
gcd(a=34, b=21)
gcd(a=55, b=34)
<module>()

通过观察上述运行结果,我们可以想象出函数调用栈的结构。

验证

我们可以用如下的代码来简单验证欧几里得算法的计算结果

import matplotlib.pyplot as plt

def gcd(a, b):
    if (a, b) == (0, 0):
        raise ValueError("a and b cannot be both 0")
    if b == 0:
        return a
    return gcd(b, a % b)

def plot_gcd_results(nums, gcd_results, b):
    # plot_gcd_results 里的代码是 trae 帮我生成的,我做了些小调整
    plt.figure(figsize=(12, 8))
    plt.bar(nums, gcd_results, color='skyblue', edgecolor='black')
    plt.xlabel('a', fontsize=12)
    plt.ylabel('GCD(a, %d)' % b, fontsize=12)
    plt.title('GCD(a, %d) for a from %d to %d' % (b, nums[0], nums[-1]), fontsize=14)
    plt.xticks(nums)
    plt.yticks(range(max(gcd_results) + 1))
    plt.grid(axis='y', linestyle='--')
    plt.tight_layout()
    plt.show()

if __name__ == "__main__":
    b = 4
    nums = range(5 * b + 1)
    gcd_results = [gcd(num, b) for num in nums]
    plot_gcd_results(nums, gcd_results, b)

运行结果如下图所示

image.png

可以看到,gcd(a,4)gcd(a,4) 的值,以 44 为周期的(值是 4,1,2,1,4,1,2,1,\cdots),这与我们前面分析的结果是一致的。

可以将第 2424 行的 bb 变量改为其他值来进行验证,这里就不赘述了。

image.png

参考资料