Featured image of post ユークリッドの互除法:最大公約数を高速に求める、紀元前から続く人類最古のアルゴリズム

ユークリッドの互除法:最大公約数を高速に求める、紀元前から続く人類最古のアルゴリズム

最大公約数(GCD)を計算するユークリッドの互除法について、その仕組みから実装、計算量、ラメの定理、拡張ユークリッドの互除法、RSA暗号への応用まで徹底解説します。

ユークリッドの互除法とは

ユークリッドの互除法Euclidean algorithm)は、2つの自然数(または整数)の最大公約数(Greatest Common Divisor, GCD)を効率的に求めるためのアルゴリズムです。紀元前300年頃に古代ギリシャの数学者ユークリッドが著した数学書『原論』(Elements)の第7巻に記載されており、「人類最古のアルゴリズム」の一つとしても広く知られています。

最大公約数を求める最も素朴な方法は、2つの数をそれぞれ素因数分解し、共通する素因数を掛け合わせることですが、数が大きくなると素因数分解そのものの計算量が膨大になり、現実的な時間で解くことが困難になります。一方、 ユークリッドの互除法 を用いれば、数千桁に及ぶ巨大な数同士であっても、極めて高速に最大公約数を計算することが可能です。

基本的な定理と仕組み

2つの自然数 $a$ と $b$ ($a \ge b$)の最大公約数を $\gcd(a, b)$ と表します。 ユークリッドの互除法は、以下の単純な定理に基づいています。

$$ a = bq + r \implies \gcd(a, b) = \gcd(b, r) $$

つまり、「 $a$ を $b$ で割ったときの商を $q$ 、余りを $r$ としたとき、 $a$ と $b$ の最大公約数は、 $b$ と $r$ の最大公約数に等しい」という性質です。

定理の証明

なぜ $\gcd(a, b) = \gcd(b, r)$ が成り立つのでしょうか。簡単に証明してみましょう。

  1. $a$ と $b$ の任意の公約数を $d$ とします。このとき、 $a = md, b = nd$ ($m, n$ は整数)と表せます。
  2. $a = bq + r$ より、 $r = a - bq$ となります。
  3. これに代入すると、 $r = md - (nd)q = d(m - nq)$ となります。
  4. $m - nq$ は整数なので、 $d$ は $r$ の約数でもあります。よって、 $a$ と $b$ の公約数 $d$ は、 $b$ と $r$ の公約数でもあります。
  5. 逆に、 $b$ と $r$ の公約数を $e$ とすると、 $b = k e, r = l e$ と表せます。
  6. $a = bq + r = (k e)q + l e = e(kq + l)$ となり、 $e$ は $a$ の約数になります。よって、 $b$ と $r$ の公約数 $e$ は、 $a$ と $b$ の公約数でもあります。
  7. したがって、 $\{a, b\}$ の公約数の集合と $\{b, r\}$ の公約数の集合は完全に一致し、その最大値である最大公約数も等しくなります。 $\blacksquare$

アルゴリズムのフローチャート

この性質を利用し、余りが $0$ になるまで割り算を繰り返すのがユークリッドの互除法です。

  flowchart TD
    Start["開始: a, b を入力"] --> Check{"b == 0 ?"}
    Check -- "Yes" --> End["GCD は a"]
    Check -- "No" --> Calc["r = a % b"]
    Calc --> Update["a = b, b = r"]
    Update --> Check

具体例での計算プロセス

例として、 $a = 1071$ と $b = 1029$ の最大公約数を求めてみましょう。

  1. $1071 \div 1029 = 1 \cdots 42$ ($a=1029, b=42$ に更新)
  2. $1029 \div 42 = 24 \cdots 21$ ($a=42, b=21$ に更新)
  3. $42 \div 21 = 2 \cdots 0$ (余りが $0$ になったので終了)

最後に割る数として残った $21$ が、 $1071$ と $1029$ の最大公約数です。

プログラムによる実装

Pythonでの実装

Pythonでは、再帰関数を用いる方法と、 while ループを用いる方法があります。ループを用いる方が関数呼び出しのオーバーヘッドがなく高速です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def gcd_loop(a: int, b: int) -> int:
    """
    ループを用いたユークリッドの互除法の実装
    """
    while b != 0:
        a, b = b, a % b
    return a

def gcd_recursive(a: int, b: int) -> int:
    """
    再帰を用いたユークリッドの互除法の実装
    """
    if b == 0:
        return a
    return gcd_recursive(b, a % b)

print(gcd_loop(1071, 1029))  # 出力: 21

C++での実装

C++17以降では <numeric> ヘッダに std::gcd が標準実装されていますが、自作する場合は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>

// 最大公約数を計算する関数(再帰版)
int gcd(int a, int b) {
    if (b == 0) {
        return a;
    }
    return gcd(b, a % b);
}

int main() {
    std::cout << "GCD: " << gcd(1071, 1029) << std::endl; // 出力: 21
    return 0;
}

計算量とラメの定理

ユークリッドの互除法はどれくらい速いのでしょうか。この計算量については、1844年にフランスの数学者ガブリエル・ラメによって証明された ラメの定理Lamé’s theorem)が有名です。

ラメの定理 2つの自然数 $a, b$ ($a > b$)に対してユークリッドの互除法を適用したときの割り算の回数は、 $b$ の十進法での桁数の $5$ 倍以下である。

これにより、アルゴリズムの時間計算量は $O(\log(\min(a, b)))$ となります。

最悪のケース(最も割り算の回数が多くなるケース)は、フィボナッチ数列の隣り合う2項を与えた場合です。例えば、 $F_{n+2}$ と $F_{n+1}$ の最大公約数を求める過程は、常に商が $1$ になり、次々に小さいフィボナッチ数へと移行していきます。

拡張ユークリッドの互除法

最大公約数を求めるだけでなく、以下のベズーの等式(Bézout’s identity)を満たす整数 $x, y$ を求めるアルゴリズムに拡張したものを、 拡張ユークリッドの互除法 (Extended Euclidean algorithm)と呼びます。

$$ ax + by = \gcd(a, b) $$

拡張ユークリッドの互除法の実装

再帰呼び出しから戻る過程で、 $x, y$ の係数を逆算していきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def ext_gcd(a: int, b: int) -> tuple[int, int, int]:
    """
    ax + by = gcd(a, b) を満たす (gcd, x, y) を返す関数
    """
    if b == 0:
        return a, 1, 0
    
    g, x1, y1 = ext_gcd(b, a % b)
    x = y1
    y = x1 - (a // b) * y1
    
    return g, x, y

g, x, y = ext_gcd(111, 30)
print(f"gcd: {g}, x: {x}, y: {y}")
# 出力: gcd: 3, x: 3, y: -11
# 確認: 111 * 3 + 30 * (-11) = 333 - 330 = 3

現代社会における応用(RSA暗号など)

拡張ユークリッドの互除法は、単なる数学パズルではなく、現代のインターネット社会を支える不可欠な技術です。 代表的な例が RSA暗号 です。RSA暗号の鍵生成プロセスでは、ある数 $e$ と、オイラー関数 $\phi(N)$ に対して、 $e d \equiv 1 \pmod{\phi(N)}$ を満たす秘密鍵 $d$ (モジュラ逆元)を求める必要があります。 これは $ed + k\phi(N) = 1$ という形に変形できるため、まさに拡張ユークリッドの互除法を用いて高速に $d$ を計算することができます。

まとめ

ユークリッドの互除法は、紀元前という遥か昔に発見されながら、その無駄のないロジックと計算効率の高さから、現代のコンピューターサイエンスの根幹を支え続けています。アルゴリズムを学ぶ上で最初に触れることが多いテーマですが、その背後には数学的な美しさと実用性が詰まっています。

comments powered by Disqus