欧几里得算法

计算最大公约数的欧几里得算法(Euclidean algorithm for computing the greatest common divisor)

给定两个非负整数 a a a b b b ,我们必须找到它们的 GCD (最大公约数)。它通常表示为 gcd ⁡ ( a , b ) \gcd(a, b) gcd(a,b) 。数学上定义为:

gcd ⁡ ( a , b ) = max ⁡ k = 1 … ∞   :   k ∣ a   ∧ k   ∣ b k . \gcd(a, b) = \max_ {k = 1 \dots \infty ~ : ~ k \mid a ~ \wedge k ~ \mid b} k. gcd(a,b)=k=1 : ka k bmaxk.

(这里的符号 “|” 表示可分性,即“ k ∣ a k|a ka ” 意为 “ k k k 可以分解出 a a a ”)

当其中一个数为零而另一个非零时,根据定义,它们的最大公约数是第二个数。当两个数都为零时,它们的最大公约数是未定义的(可以是任意大的数),但我们可以将其定义为零。这给了我们一个简单的规则:如果一个数字为零,则最大公约数是另一个数字。

下面讨论的欧几里得算法将在 O ( log ⁡ min ⁡ ( a , b ) ) O(\log \min(a, b)) O(logmin(a,b)) 的时间内找到两个数 a a a b b b 的最大公约数。

该算法首先在欧几里得的“元素”(大约公元前 300 年)中被描述,但该算法可能有更早的起源。


算法

最初,欧几里得算法的公式如下:从较大的数字中减去较小的数字,直到其中一个数字为零。事实上,如果 g g g 除以 a a a b b b ,它也除以 a − b a-b ab 。另一方面,如果 g g g 除以 a − b a-b ab b b b ,那么它也除以 a = b + ( a − b ) a=b+(a-b) a=b+(ab) ,这意味着 { a , b } \{a, b\} {a,b} { b , a − b } \{b,a-b\} {b,ab} 的公约数集是重合的。

请注意,在 b b b 至少被减去 ⌊ a b ⌋ \left\lfloor\frac{a}{b}\right\rfloor ba 次之前, a a a 仍然是较大的数字。因此,为了加快速度, a − b a-b ab 被替换为 a − ⌊ a b ⌋ b = a   m o d   b a-\left\lfloor\frac{a}{b}\right\rfloor b = a \bmod b abab=amodb 。然后以极其简单的方式制定算法:

gcd ⁡ ( a , b ) = { a , b = 0 gcd ⁡ ( b , a   m o d   b ) , 其他情况 \gcd(a, b) = \begin{cases}a,&b = 0 \\ \gcd(b, a \bmod b),&\text{其他情况}\end{cases} gcd(a,b)={a,gcd(b,amodb),b=0其他情况


实现

int gcd (int a, int b) {
    if (b == 0)
        return a;
    else
        return gcd (b, a % b);
}

使用 C++ 中的三元运算符,我们可以将其编写为单行。

int gcd (int a, int b) {
    return b ? gcd (b, a % b) : a;
}

最后,这是一个非递归实现:

int gcd (int a, int b) {
    while (b) {
        a %= b;
        swap(a, b);
    }
    return a;
}

请注意,从 C++17 开始,在 C++ 中gcd作为标准函数实现。


时间复杂度

该算法的运行时间由拉梅定理估计,该定理在欧几里得算法和斐波那契数列之间建立了惊人的联系:

如果 a > b ≥ 1 a > b \geq 1 a>b1 b < F n b < F_n b<Fn 对于一些 n n n ,欧几里得算法最多执行 n − 2 n-2 n2 次递归调用。

此外,可以证明该定理的上界是最优的。 当 a = F n a = F_n a=Fn b = F n − 1 b = F_{n-1} b=Fn1 , g c d ( a , b ) gcd(a, b) gcd(a,b)将正好执行 n − 2 n-2 n2 次递归调用。换句话说,连续的斐波那契数是欧几里得算法的最坏输入情况。

鉴于斐波那契数呈指数增长,我们得到欧几里得算法复杂度为 O ( log ⁡ min ⁡ ( a , b ) ) O(\log \min(a, b)) O(logmin(a,b))

另一种估计复杂度的方法是注意到 a ≥ b a \geq b ab 情况下的 a   m o d   b a \bmod b amodb 至少比 a a a 2 2 2 倍,因此在算法的每次迭代中,较大的数字至少减少一半。


最小公倍数

计算最小公倍数(通常表示为 LCM )可以简化为使用以下简单公式计算 GCD

lcm ( a , b ) = a ⋅ b gcd ⁡ ( a , b ) \text{lcm}(a, b) = \frac{a \cdot b}{\gcd(a, b)} lcm(a,b)=gcd(a,b)ab

因此,可以使用具有相同时间复杂度的欧几里得算法计算 LCM

这里给出了一种实现,它通过先用 GCD 除以 a a a ,巧妙地避免了整数溢出。

int lcm (int a, int b) {
    return a / gcd(a, b) * b;
}

二进制 GCD

Binary GCD 算法是对普通 Eulidean 算法的优化。

正常算法的缓慢部分是模运算。模运算,尽管我们将它们视为 O ( 1 ) O(1) O(1) , 但它其实比简单的操作(如加法、减法或按位操作)慢得多。所以最好避免这些。

事实证明,您可以设计一种避免模运算的快速 GCD 算法。它基于几个属性:

  • 如果两个数字都是偶数,那么我们可以将两者都分解并计算剩余数字的 GCD: gcd ⁡ ( 2 a , 2 b ) = 2 gcd ⁡ ( a , b ) \gcd(2a, 2b) = 2 \gcd(a, b) gcd(2a,2b)=2gcd(a,b)
  • 如果其中一个数字是偶数而另一个是奇数,那么我们可以从偶数中删除因子 2: gcd ⁡ ( 2 a , b ) = gcd ⁡ ( a , b ) \gcd(2a, b) = \gcd(a, b) gcd(2a,b)=gcd(a,b) b b b 为奇数。
  • 如果两个数字都是奇数,那么减去另一个数字中的一个数字不会改变 GCD: gcd ⁡ ( a , b ) = gcd ⁡ ( b , a − b ) \gcd(a, b) = \gcd(b, a-b) gcd(a,b)=gcd(b,ab)

仅使用这些属性,以及来自 GCC 的一些快速按位函数,我们可以实现一个快速版本:

int gcd(int a, int b) {
    if (!a || !b)
        return a | b;
    unsigned shift = __builtin_ctz(a | b);
    a >>= __builtin_ctz(a);
    do {
        b >>= __builtin_ctz(b);
        if (a > b)
            swap(a, b);
        b -= a;
    } while (b);
    return a << shift;
}

请注意,这种优化通常不是必需的,并且大多数编程语言在其标准库中已经具有 GCD 功能。例如 C++17在 numeric 头文件中有 std::gcd 这样的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我真的不是cjc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值