欧几里得算法(Euclidean algorithm)的证明与实现

584 阅读2分钟

欧几里得算法(Euclidean algorithm)的证明与实现

欧几里得算法也称为辗转相除法, 用于计算两个正整数的最大公因数, 一般用(a,b)(a, b)来表示a和b的最大公因数, 计算机领域一般表示成gcd(a,b)gcd(a, b).

首先,我们给出欧几里得算法依赖的一些基本概念的定义:

定义(整除) 如果aabb为整数且a0a \neq 0,我们说aa整除bb是指存在整数cc使得b=acb = a \cdot c。如果aa整除bb,我们还称aabb的一个因子,且称bbaa的倍数

如果aa整除bb,则将其记为aba \mid b,如果aa不能整除bb,则记其为aba \nmid b

定义(最大公因子) 不全为零的整数aabb的最大公因子是指能够同时整除aabb的最大整数。

aabb的最大公因子记作(a,b)(a, b)。有时也记作gcd(a,b)gcd(a, b)

注意当nn为正整数时,(0,n)=(n,0)=n(0, n) = (n, 0) = n

虽然所有的正整数都能够整除00,我们还是定义(0,0)=0(0, 0) = 0。这样可以确保关于最大公因子的相关结论在所有情况下均成立。


然后我们给出欧几里得算法的定理描述和证明过程

欧几里得算法的定理描述如下:

定理(欧几里得算法) 令整数r0=ar_{0} = ar1=br_{1} = b满足ab>0a \ge b > 0, 如果连续做带余除法得到rj=rj+1qj+1+rj+2r_j = r_{j+1} \cdot q_{j+1} + r_{j+2}, 且0<rj+2<rj+10 < r_{j+2} < r_{j+1}j=0,1,2,,n2j = 0, 1, 2, \cdots, n-2),rn+1=0r_{n+1} = 0, 那么(a,b)=rn(a, b) = r_n,它是最后一个非零余数。

从定理中我们看到通过连续应用带余除法,在每一步中被除数和除数被更小的数代替(这些更小的数实际上是每一步中的除数和余数),运算直到余数为零时终止。 这一系列运算产生了一系列的等式,而最大公因子就是最后一个非零的余数。

接下来我们看看欧几里得算法的定理证明:

证明r0=ar_{0} = ar1=br_{1} = b是正整数且满足aba \ge b,那么通过连续运用带余除法,我们求得

r0=r1q1+r20r2<r1r1=r2q2+r30r3<r2rj=rj+1qj+1+rj+20rj+2<rj+1rn3=rn2qn2+rn10rn1<rn2rn2=rn1qn1+rn0rn<rn1rn1=rnqn\begin{aligned} r_{0} & = r_{1} \cdot q_{1} + r_{2} & 0 \le r_{2} < r_{1} \\ r_{1} & = r_{2} \cdot q_{2} + r_{3} & 0 \le r_{3} < r_{2} \\ & \vdots & \\ r_{j} & = r_{j+1} \cdot q_{j+1} + r_{j+2} & 0 \le r_{j+2} < r_{j+1} \\ & \vdots & \\ r_{n-3} & = r_{n-2} \cdot q_{n-2} + r_{n-1} & 0 \le r_{n-1} < r_{n-2} \\ r_{n-2} & = r_{n-1} \cdot q_{n-1} + r_{n} & 0 \le r_{n} < r_{n-1} \\ r_{n-1} & = r_{n} \cdot q_{n} \end{aligned}

可以假设最终一定会有一个余数为零,这是因为余数组成的序列a=r0r1>r2>0a = r_{0} \ge r_{1} > r_{2} > \cdots \ge 0所包含的项的个数不会大于aa(因为每个余数都是整数)。 由引理1,我们得到(a,b)=(r0,r1)=(r1,r2)=(r2,r3)==(rn2,rn1)=(rn1,rn)=(rn,0)=rn(a, b) = (r_{0}, r_{1}) = (r_{1}, r_{2}) = (r_{2}, r_{3}) = \cdots = (r_{n-2}, r_{n-1}) = (r_{n-1}, r_{n}) = (r_{n}, 0) = r_{n}。因此(a,b)=rn(a, b) = r_{n}, 这是最后一个非零余数。\blacksquare

引理1 如果eedd是整数且e=dq+re = d \cdot q + r,其中qqrr是整数,那么(e,d)=(d,r)(e, d) = (d, r)

证明 在定理1中,取a=ra = rb=db = dc=qc = q,(即(r+qd,d)=(r,d)(r + q \cdot d, d) = (r, d)),那么由定理1可以直接得到引理。\blacksquare

定理1aabbcc是整数,那么(a+cb,b)=(a,b)(a + c \cdot b, b) = (a, b)

证明aabbcc是整数。我们将证明aabb的公因子与a+cba + c \cdot bbb的公因子相同,即证明(a+cb,b)=(a,b)(a + c \cdot b, b) = (a, b)。 令eeaabb的公因子。由定理2可知e(a+cb)e \mid (a + c \cdot b),所以eea+cba + c \cdot bbb的公因子。 如果ffa+cba + c \cdot bbb的公因子,那么由定理2可知ff整除(a+cb)cb=a(a + c \cdot b) - c \cdot b = a,所以ffaabb的公因子。 因此(a+cb,b)=(a,b)(a + c \cdot b, b) = (a, b)\blacksquare

定理2 如果aabbmmnn为整数,且cac \mid acbc \mid b,则c(ma+nb)c \mid (m \cdot a + n \cdot b)

证明 因为cac \mid acbc \mid b,故存在整数eeff,使得a=cea = c \cdot eb=cfb = c \cdot f。 因此ma+nb=mce+ncf=c(me+nf)m \cdot a + n \cdot b = m \cdot c \cdot e + n \cdot c \cdot f = c (m \cdot e + n \cdot f)。从而,c(ma+nb)c \mid (m \cdot a + n \cdot b)\blacksquare

我们举下面的例子来说明欧几里得算法的具体步骤。

例子1 用欧几里得算法求(252,198)(252, 198)的步骤如下:

252=198×1+54198=54×3+3654=36×1+1836=18×2\begin{aligned} 252 & = 198 \times 1 + 54 \\ 198 & = 54 \times 3 +36 \\ 54 & = 36 \times 1 + 18 \\ 36 & = 18 \times 2 \end{aligned}

我们将这些步骤总结在下表中:

jjrjr_{j}rj+1r_{j+1}qj+1q_{j+1}rj+2r_{j+2}
0252198154
119854336
25436118
3361820

最后一个非零除数(在最后一列倒数第二行的那个数)就是252和198的最大公因子。因此(252,198)=18(252, 198) = 18\blacktriangleleft


最后,我们给出欧几里得算法的python语言实现:

  • 递归版本
def Euclid(a:int, b:int):
    assert a >= b >= 0
    if b == 0:
        return a
    return Euclid(b, a%b)

def gcd(a:int, b:int):
    assert a >= 0 and b >= 0
    if a < b:
        a, b = b, a # swap(a, b)
    return Euclid(a, b)

def print_gcd(a, b):
    print("gcd({}, {}) = {}".format(a, b, gcd(a, b)))

if __name__ == "__main__":
    print_gcd(252, 198)
  • 迭代版本
def Euclid(a:int, b:int):
    assert a >= b >= 0
    r = a % b
    while r != 0:
        a = b
        b = r
        r = a % b
    return b

def gcd(a:int, b:int):
    assert a >= 0 and b >= 0
    if a < b:
        a, b = b, a # swap(a, b)
    return Euclid(a, b)

def print_gcd(a, b):
    print("gcd({}, {}) = {}".format(a, b, gcd(a, b)))

if __name__ == "__main__":
    print_gcd(252, 198)

另外,python标准库的math模块内置了gcd的实现,可以作为基准版本:

import math

def print_gcd(a, b):
    print("gcd({}, {}) = {}".format(a, b, math.gcd(a, b)))

if __name__ == "__main__":
    print_gcd(252, 198)

参考文献:

  • 初等数论及其应用(原书第6版): ISBN 978-7-111-48697-8
  • 离散数学及其应用(原书第8版): ISBN 978-7-111-63687-8
  • 算法概论: ISBN 978-7-302-17939-9