ユークリッドの互除法と拡張ユークリッドの互除法のpythonプログラム

ユークリッドの互除法・拡張ユークリッドの互除法を、正当性の帰納法による証明・ベズーの等式の導出・計算量O(log n)の証明(ラメの定理)・RSA暗号のモジュラ逆元への応用まで、実行検証付きでPythonで解説します。

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

ユークリッドの互除法とは、2つの整数 \(a\) と \(b\) \((a>b)\) が与えられたとき、\(a\) を \(b\) で割った余り \(r\) を利用することで、\(a\) と \(b\) の最大公約数 \(\gcd(a,b)\) を求める方法。除法の原理を利用し、割り算を繰り返すことによって最大公約数を求める。紀元前300年頃のユークリッド『原論』に記載がある、現存する中で最古のアルゴリズムの一つとされる。

本記事では、単にアルゴリズムを提示するだけでなく、

  1. なぜこの手続きが必ず正しい最大公約数を返すのか(正当性の証明)
  2. なぜ必ず有限回で停止するのか、その回数のオーダー(計算量の証明)
  3. 拡張版がなぜ \(ax+by=\gcd(a,b)\) の解を実際に構成できるのか(ベズーの等式の証明)
  4. RSA暗号などの公開鍵暗号でどう使われるのか(モジュラ逆元への応用)

を、実行して得た数値による検証とあわせて順に見ていく。

記法・前提

以降、断りがない限り \(a,b\) は整数、少なくとも一方は \(0\) でないものとする。\(\gcd(a,b)\) は \(a\) と \(b\) の最大公約数(両方を割り切る整数のうち最大のもの)を表す。\(\gcd(a,0)=|a|\) 、\(\gcd(0,0)\) は未定義とする。

補題:\(\gcd(a,b) = \gcd(b, a \bmod b)\)

ユークリッドの互除法が正しく動作する根拠となる、最も重要な補題を証明する。

補題. \(a, b\) を整数とし、\(b \neq 0\) とする。\(r = a \bmod b\) (すなわち \(a = bq + r\) 、\(0 \le r < |b|\) を満たす整数 \(q, r\) )とすると、

\[ \gcd(a, b) = \gcd(b, r) \tag{1} \]

証明. \(a = bq + r\) という関係から、\(a\) と \(b\) の公約数の集合が、\(b\) と \(r\) の公約数の集合と完全に一致することを示す。

  • (\(\subseteq\) ) \(d\) を \(a\) と \(b\) の公約数とする。\(d \mid a\) かつ \(d \mid b\) より、\(r = a - bq\) は \(d\) で割り切れる整数の線形結合なので \(d \mid r\) 。よって \(d\) は \(b\) と \(r\) の公約数でもある。
  • (\(\supseteq\) ) \(d\) を \(b\) と \(r\) の公約数とする。\(d \mid b\) かつ \(d \mid r\) より、\(a = bq + r\) も \(d\) で割り切れる整数の線形結合なので \(d \mid a\) 。よって \(d\) は \(a\) と \(b\) の公約数でもある。

したがって \(a,b\) の公約数の集合と \(b,r\) の公約数の集合は一致し、特にその最大値である最大公約数も一致する。\(\blacksquare\)

この補題により、「\(\gcd(a,b)\) を求める」問題を、より小さい数の組 \(\gcd(b, a \bmod b)\) を求める問題に帰着できる。これを繰り返すことがユークリッドの互除法の本質である。

ユークリッドの互除法のアルゴリズム

入力:整数\(a,b\)
出力:最大公約数 \(d\)

  1. \(a_0 = a\) , \(a_1 = b\)
  2. \(a_i=0\) のとき,
    \(d=a_{i-1}\) とし終了
  3. \(a_{i-1}=a_iq_i+a_{i+1}\)
      として2に戻る

停止性と正当性の証明

上記のアルゴリズムが(a)必ず有限回で停止し、(b)停止したときに返す値が真に \(\gcd(a,b)\) であることを、それぞれ証明する。

(a)停止性. 手順3で生成される列 \(a_0, a_1, a_2, \dots\) は、剰余の定義(\(0 \le a_{i+1} < a_i\) 、\(a_i \neq 0\) の場合)により、\(a_i \neq 0\) である限り

\[ a_1 > a_2 > a_3 > \cdots \ge 0 \]

という狭義単調減少な非負整数の列になる。非負整数には無限に狭義単調減少する列は存在しない(整列性)ため、有限のステップ数 \(n\) で必ず \(a_{n}=0\) となり、アルゴリズムは停止する。

(b)正当性(強い帰納法). 「任意の \(i \ge 1\) に対して \(\gcd(a_{i-1}, a_i) = \gcd(a, b)\) が成り立つ」ことを \(i\) に関する帰納法で示す。

  • 基底: \(i=1\) のとき \(\gcd(a_0,a_1)=\gcd(a,b)\) は定義よりそのまま成立。
  • 帰納段階: \(\gcd(a_{i-1},a_i)=\gcd(a,b)\) が成り立つと仮定する(帰納法の仮定)。手順3の \(a_{i-1}=a_iq_i+a_{i+1}\) に補題(1)を適用すると \(\gcd(a_{i-1},a_i)=\gcd(a_i,a_{i+1})\) 。帰納法の仮定と合わせて \(\gcd(a_i,a_{i+1})=\gcd(a,b)\) を得る。

よって全ての \(i\) について不変量 \(\gcd(a_{i-1},a_i)=\gcd(a,b)\) が成立する。アルゴリズムが停止するステップ、すなわち \(a_i=0\) となる最小の \(i\) において、\(\gcd(a_{i-1},a_i)=\gcd(a_{i-1},0)=a_{i-1}\) 。不変量よりこれは \(\gcd(a,b)\) に等しい。したがって出力 \(d=a_{i-1}\) は真に \(\gcd(a,b)\) である。\(\blacksquare\)

プログラム

def euclid(a,b):
    a_list = []
    if a < b:
        a_list.append(b)
        a_list.append(a)
    if a >= b:
        a_list.append(a)
        a_list.append(b)
    i = 0
    while(a_list[-1]!=0):
        a_list.append(a_list[i]%a_list[i+1])
        i +=1
    return a_list[-2]

拡張ユークリッドの互除法とベズーの等式

拡張ユークリッドの互除法とは、一次不定方程式 \(ax+by=d\) (\(d=\gcd(a,b)\) )の一つの解を求める方法。\(a_0=a\) 、\(a_1=b\) とおくと、以下のように求めることができる。

\([\begin{array}{cc} a_{i-1} \\ a_i \end{array}]= [\begin{array}{cc} a_iq_i+a_{i+1} \\ a_i \end{array}]\) とすると, \([\begin{array}{cc} a_{i-1} \\ a_i \end{array}]= [\begin{array}{cc} q_i & 1 \\ 1 & 0 \end{array}] [\begin{array}{cc} a_i \\ a_{i+1} \end{array}] \) とかける. \([\begin{array}{cc} q_i & 1 \\ 1 & 0 \end{array}]\) の逆行列を,\(L_i\) とする. \([\begin{array}{cc} a_i \\ a_{i+1} \end{array}]=L_i [\begin{array}{cc} a_{i-1} \\ a_i \end{array}] \) これを繰り返すと, \([\begin{array}{cc} d \\ 0 \end{array}]=L_i,\dots,L_2 [\begin{array}{cc} a \\ b \end{array}] \) となる.

この行列表現は、拡張ユークリッドの互除法が「線形写像の合成」として理解できることを示しているが、実際に係数 \(x,y\) を計算するときは、次に述べる係数追跡(coefficient tracking)の形で実装するのが標準的である。

拡張ユークリッドの互除法のアルゴリズム

入力:整数\(a,b\)
出力:最大公約数\(d\) と \(ax+by=d\) となる整数\(x, y\)

  1. \(a_0 =a\) , \(a_1 =b\)
  2. \(x_0 =1\) , \(x_1 =0\) ,\(y_0 =0\) , \(y_1 =1\)
  3. \(a_i=0\) のとき,
    \(d=a_{i−1}\) ,\(x=x_{i−1}\) ,\(y=y_{i−1}\) とし終了.
  4. \(a_{i−1} = a_iq_i + a_{i+1}\) により,\(a_{i+1}\) と\(q_i\) を定める.
    \(x_{i+1} = x_{i−1} − q_ix_i\) \(y_{i+1} = y_{i−1} − q_iy_i\)
    として3に戻る.

プログラム

def exEuclid(a,b):
    a_list = []
    if a < b:
        a_list.append(b)
        a_list.append(a)
    if a >= b:
        a_list.append(a)
        a_list.append(b)
    q = []
    x = []
    x.append(1)
    x.append(0)
    y = []
    y.append(0)
    y.append(1)
    i = 0
    while(a_list[-1]!=0):
        a_list.append(a_list[i]%a_list[i+1])
        q.append(a_list[i]//a_list[i+1])
        x.append(x[-2]-q[-1]*x[-1])
        y.append(y[-2]-q[-1]*y[-1])
        i +=1
    return x[-2],y[-2],a_list[-2]

ベズーの等式の証明

ベズーの等式. 任意の整数 \(a,b\) (少なくとも一方は \(0\) でない)に対して、\(ax+by=\gcd(a,b)\) を満たす整数 \(x,y\) が存在する。

これを「存在を示すだけ」ではなく、拡張ユークリッドの互除法が実際にそのような \(x,y\) を構成することを、係数追跡の不変量に関する帰納法で証明する。

主張. アルゴリズム中の全ての \(i \ge 0\) について、次の不変量が成り立つ。

\[ a \cdot x_i + b \cdot y_i = a_i \tag{2} \]

証明(帰納法).

  • 基底 \(i=0\) : \(x_0=1, y_0=0, a_0=a\) より \(a\cdot 1 + b\cdot 0 = a\) 。成立。
  • 基底 \(i=1\) : \(x_1=0, y_1=1, a_1=b\) より \(a\cdot 0 + b\cdot 1 = b\) 。成立。
  • 帰納段階: \(i-1\) と \(i\) で不変量(2)が成り立つと仮定する。手順4の定義 \(x_{i+1}=x_{i-1}-q_ix_i\) 、\(y_{i+1}=y_{i-1}-q_iy_i\) を使うと、
\[ \begin{aligned} a\cdot x_{i+1} + b\cdot y_{i+1} &= a(x_{i-1}-q_ix_i) + b(y_{i-1}-q_iy_i) \\ &= (a x_{i-1} + b y_{i-1}) - q_i(a x_i + b y_i) \\ &= a_{i-1} - q_i a_i \quad (\text{帰納法の仮定}) \\ &= a_{i+1} \quad (\because a_{i-1}=a_iq_i+a_{i+1}) \end{aligned} \]

よって \(i+1\) でも不変量(2)が成立する。強い帰納法(2段階の漸化式なので \(i-1,i\) を仮定して \(i+1\) を示す形)により、全ての \(i\) で不変量(2)が成り立つ。\(\blacksquare\)

アルゴリズムが停止するステップ(\(a_i=0\) となる最初の \(i\) )では、出力は \(d=a_{i-1}\) 、\(x=x_{i-1}\) 、\(y=y_{i-1}\) であり、不変量(2)をそのステップ番号 \(i-1\) について適用すると、

\[ a\cdot x + b\cdot y = a\cdot x_{i-1} + b\cdot y_{i-1} = a_{i-1} = d = \gcd(a,b) \]

を得る。これがまさにベズーの等式であり、拡張ユークリッドの互除法はその解 \((x,y)\) を実際に構成的に与えるアルゴリズムである。\(\blacksquare\)

別記法との対応. 文献やライブラリによっては、この係数追跡を \((old\_r, old\_s, old\_t)/(r,s,t)\) という変数名で1ステップずつ更新する反復形式で書くことが多い(例えば RSA暗号の記事extended_gcd は再帰形式で等価な計算をしている)。対応関係は \(old\_r \leftrightarrow a_{i-1}\) 、\(r \leftrightarrow a_i\) 、\(old\_s \leftrightarrow x_{i-1}\) 、\(s \leftrightarrow x_i\) 、\(old\_t \leftrightarrow y_{i-1}\) 、\(t \leftrightarrow y_i\) である。反復形式のコードは次の通り(検証コードでもこの実装を使用する)。

def extended_gcd(a, b):
    """反復形式の拡張ユークリッド互除法。gcd, x, y, ステップ数を返す。
    a*x + b*y = gcd(a, b) を満たす。
    """
    old_r, r = a, b
    old_s, s = 1, 0
    old_t, t = 0, 1
    steps = 0
    while r != 0:
        q = old_r // r
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
        old_t, t = t, old_t - q * t
        steps += 1
    return old_r, old_s, old_t, steps

計算量:ラメの定理と最悪計算量 \(O(\log(\min(a,b)))\)

ユークリッドの互除法が停止することは前節で示したが、実用上重要なのは何ステップで停止するかである。ここでは、最悪ケースがフィボナッチ数列の連続する2項であることを示す**ラメの定理(Lamé’s theorem, 1844年)**を証明し、数値実験で検証する。

定理(ラメの定理)

\(a > b > 0\) に対しユークリッドの互除法が \(n\) ステップ(\(n\) 回の除算)で停止するとき、

\[ a \ge F_{n+2}, \qquad b \ge F_{n+1} \tag{3} \]

が成り立つ。ここで \(F_k\) は \(F_1=F_2=1\) 、\(F_{k}=F_{k-1}+F_{k-2}\) で定義されるフィボナッチ数列である。

証明(後ろ向き帰納法). アルゴリズムの列を \(r_0=a, r_1=b, r_2, \dots, r_n = \gcd(a,b) \ge 1\) 、\(r_{n+1}=0\) とする(\(n\) ステップで停止、\(r_i = r_{i-2} \bmod r_{i-1}\) )。各商 \(q_i = \lfloor r_{i-1}/r_i \rfloor\) について、最終ステップを除いて \(r_{i+1} \neq 0\) である限り \(r_{i-1} > r_i\) が成り立つため \(q_i \ge 1\) 、したがって

\[ r_{i-1} = q_i r_i + r_{i+1} \ge r_i + r_{i+1} \qquad (1 \le i \le n-1) \tag{4} \]

が成り立つ(最後から2番目のステップ \(i=n\) では \(r_{n+1}=0\) なので等号 \(r_{n-1}=q_nr_n\) で \(q_n\ge2\) の場合もあるが、不等式 \(r_{n-1}\ge r_n+r_{n+1}=r_n\) は自明に成立する)。

\(k=0,1,\dots,n-1\) について「\(r_{n-k} \ge F_{k+2}\) 」を \(k\) に関する帰納法で示す。

  • \(k=0\) : \(r_n = \gcd(a,b) \ge 1 = F_2\) 。
  • \(k=1\) : \(r_{n-1} \ge r_n + r_{n+1} = r_n + 0 \ge 1\) 、かつ \(r_{n-1}>r_n\ge1\) なので \(r_{n-1}\ge2=F_3\) 。
  • 帰納段階: \(r_{n-k}\ge F_{k+2}\) かつ \(r_{n-k+1}\ge F_{k+1}\) を仮定すると、式(4)より \(r_{n-k-1} \ge r_{n-k}+r_{n-k+1} \ge F_{k+2}+F_{k+1}=F_{k+3}\) 。

\(k=n-1\) とすると \(r_1=b\ge F_{n+1}\) 。同様に \(r_0=a=q_1r_1+r_2\ge r_1+r_2\ge F_{n+1}+F_n=F_{n+2}\) (\(n\ge1\) のとき)。\(\blacksquare\)

なぜフィボナッチ数の組が最悪ケースか

式(4)の不等号 \(r_{i-1}\ge r_i+r_{i+1}\) は、商 \(q_i\) が最小値 \(1\) のときにちょうど等号になる。つまり、各ステップで商が \(1\) (=余りが最も緩やかにしか減らない)であり続ける入力こそが、同じステップ数を達成するのに必要な \(a,b\) を最小にする=同じ \(a,b\) に対してステップ数を最大化する入力である。商が恒等的に \(1\) になる列はまさにフィボナッチ数列の漸化式 \(F_{k+1}=1\cdot F_k+F_{k-1}\) そのものであり、連続するフィボナッチ数の組 \((F_{n+2},F_{n+1})\) を入力すると式(4)が全ステップで等号になる。したがってフィボナッチ数の組は、その大きさに対してユークリッドの互除法が要するステップ数を最大化する、真の最悪ケースである。

系:計算量 \(O(\log(\min(a,b)))\)

式(3)より \(b\ge F_{n+1}\) 。フィボナッチ数の一般項 \(F_k \approx \varphi^{k-2}/\sqrt5\) (\(\varphi=(1+\sqrt5)/2\) は黄金比)を用いると、\(n\) について解くことで

\[ n = O(\log_\varphi b) = O(\log b) = O(\log(\min(a,b))) \]

を得る。すなわちユークリッドの互除法のステップ数は入力の桁数(ビット長)に対して線形、つまり入力の値そのものに対しては対数オーダーである。各ステップの除算コストを \(O(1)\) (固定長整数として)とみなせば全体の計算量は \(O(\log(\min(a,b)))\) となる(多倍長整数を扱う場合は各除算のコストがビット長に依存するため、より詳細な解析ではもう少し大きくなるが、ステップ回数自体のオーダーは変わらない)。

数値検証:フィボナッチ組 vs ランダム組のステップ数

以下のコードで、各ビット長 \(b\) (8〜256ビット、8ビット刻み)について

  • ちょうど \(b\) ビットに達する最小のフィボナッチ数と、その1つ前のフィボナッチ数の組
  • 同じビット長のランダムな整数の組(200試行の平均・最大)

に対しステップ数を実測し、比較した。

import random

def gcd_steps(a, b):
    """通常のユークリッドの互除法、除算ステップ数のみ数える"""
    steps = 0
    while b != 0:
        a, b = b, a % b
        steps += 1
    return steps

def fib_pair_at_bitlength(target_bits):
    """target_bits ビットに達する最小のフィボナッチ数 F_n とその1つ前 F_{n-1} を返す"""
    a, b = 1, 1
    while b.bit_length() < target_bits:
        a, b = b, a + b
    return a, b

for bits in range(8, 257, 8):
    fa, fb = fib_pair_at_bitlength(bits)
    fib_steps = gcd_steps(fb, fa)

    trials = []
    for _ in range(200):
        x = random.getrandbits(bits) | (1 << (bits - 1)) | 1
        y = random.getrandbits(bits) | (1 << (bits - 1)) | 1
        trials.append(gcd_steps(max(x, y), min(x, y)))
    avg_steps = sum(trials) / len(trials)

    print(f"bits={bits}: fibonacci={fib_steps}, random_avg={avg_steps:.2f}")

実行結果(抜粋、全ビット長は8〜256を8刻みで実測):

ビット長フィボナッチ組のステップ数ランダム組の平均ステップ数(200試行)ランダム組の最大ステップ数
8104.959
324519.0227
649138.1952
12818375.1496
192275112.22130
256367149.38173

ユークリッドの互除法のステップ数と入力ビット長の関係。フィボナッチ組(青、最悪ケース)とランダム組の平均(緑、200試行平均)を比較すると、256ビットでフィボナッチ組は367ステップ、ランダム組平均は149.38ステップで、最悪ケースは平均ケースのおよそ2.5倍のステップ数となる

全ビット長にわたり、フィボナッチ組のステップ数はランダム組の平均のおよそ 2.3〜2.5倍で推移しており、フィボナッチ組が明確に最悪ケースになっていることが確認できる。また、フィボナッチ組のステップ数 \(n\) とラメの定理の境界 \(\log_\varphi b\) を比較すると、次のように非常に近い値になる(比が1に収束)。

ビット長実測ステップ数 \(n\)\(\log_\varphi b\)\(n / \log_\varphi b\)
324545.330.993
649191.330.996
128183183.330.998
256367367.330.999

これはラメの定理の証明で用いた不等式(3)がフィボナッチ入力に対して等号で成立する(=境界がタイトである)ことの直接的な数値的裏付けである。

なお、フィボナッチ数 \(F_{10}=55, F_9=34\) という小さな例で実際のステップを追うと、全ての商が \(1\) になり続ける様子が具体的に見える。

55 = 1*34 + 21
34 = 1*21 + 13
21 = 1*13 + 8
13 = 1*8 + 5
8  = 1*5 + 3
5  = 1*3 + 2
3  = 1*2 + 1
2  = 2*1 + 0

(55, 34)で8ステップ = \(F_{10}, F_9\) に対して \(10-2=8\) ステップという、\(\gcd(F_n,F_{n-1})\) の計算が一般に \(n-2\) ステップかかるという既知の結果とも一致する。最後の商だけ \(2\) になっている(\(F_2=2\cdot F_1+0\) )のは、\(F_1=F_2=1\) という初期条件のため以降の商がすべて \(1\) でループが続いた末に列がここで終端するからである。

連分数との関係

ユークリッドの互除法が生成する商の列 \(q_1,q_2,q_3,\dots\) は、実は \(a/b\) の連分数展開そのものである。

\[ \frac{a}{b} = q_1 + \cfrac{1}{q_2 + \cfrac{1}{q_3 + \cfrac{1}{\ddots}}} = [q_1; q_2, q_3, \dots] \]

これは、除算のたびに \(a_{i-1}/a_i = q_i + a_{i+1}/a_i\) 、すなわち \(a_{i-1}/a_i = q_i + 1/(a_i/a_{i+1})\) となることから直ちに従う(逆数を取って次の項に潜り込ませる操作を繰り返しているだけである)。実際に確認すると、

from fractions import Fraction

def euclid_quotients(a, b):
    qs = []
    while b != 0:
        q, r = divmod(a, b)
        qs.append(q)
        a, b = b, r
    return qs

def continued_fraction_quotients(x: Fraction):
    qs = []
    num, den = x.numerator, x.denominator
    while den != 0:
        q, r = divmod(num, den)
        qs.append(q)
        num, den = den, r
    return qs

for a, b in [(355, 113), (1071, 462), (49, 34)]:
    print(a, b, euclid_quotients(a, b), continued_fraction_quotients(Fraction(a, b)))

実行結果は次の通りで、両者は完全に一致する。

\(a/b\)互除法の商列連分数展開
\(355/113\)\([3, 7, 16]\)\([3; 7, 16]\)
\(1071/462\)\([2, 3, 7]\)\([2; 3, 7]\)
\(49/34\)\([1, 2, 3, 1, 3]\)\([1; 2, 3, 1, 3]\)

(\(355/113\) は円周率 \(\pi\) の非常に良い近似分数として知られており、\([3;7,16]\) という短い連分数で表せることが、この近似精度の良さの理由でもある。)この一致は偶然ではなく、ユークリッドの互除法と連分数展開が本質的に同じ再帰構造を持つことを示している。本記事では深入りしないが、有理数の最良近似分数(収束分数)の理論に興味があれば、この対応が出発点になる。

応用:RSA暗号におけるモジュラ逆元

拡張ユークリッドの互除法の最も重要な実用上の応用は、モジュラ逆元の計算である。

命題. \(\gcd(a,m)=1\) (\(a\) と \(m\) が互いに素)ならば、\(a\) の \(m\) を法とする逆元 \(a^{-1} \bmod m\) が存在し、拡張ユークリッドの互除法で計算できる。

証明. \(\gcd(a,m)=1\) なので、ベズーの等式より整数 \(x,y\) が存在して

\[ ax + my = 1 \]

両辺を \(m\) で法として見ると、\(my \equiv 0 \pmod m\) なので

\[ ax \equiv 1 \pmod m \]

したがって \(x \bmod m\) が \(a\) の \(m\) を法とする逆元である。逆元は \(\bmod m\) で一意に定まる(\(ax_1\equiv ax_2\equiv1\) かつ \(\gcd(a,m)=1\) なら \(a(x_1-x_2)\equiv0\) 、互いに素性より \(x_1\equiv x_2\) )。\(\blacksquare\)

これはまさに、 RSA暗号の記事 で秘密鍵指数 \(d\) を計算する手順そのものである。RSA では公開鍵指数 \(e\) (多くの場合 \(65537\) )に対し、\(\varphi(N)=(p-1)(q-1)\) を法とする逆元 \(d=e^{-1}\bmod\varphi(N)\) を計算する。\(e\) は \(\gcd(e,\varphi(N))=1\) となるよう選ばれるため、上記の命題により \(d\) が必ず一意に存在し、拡張ユークリッドの互除法で効率的に計算できる。

検証:RSA相当スケールでのモジュラ逆元計算

実際に、RSA暗号の記事と同じ Miller-Rabin 素数判定を用いて 1024ビットの素数を2つ(つまり \(N\) は2048ビット、実用の RSA-2048 相当のスケール)生成し、\(e=65537\) の \(\varphi(N)\) を法とする逆元 \(d\) を、本記事の反復形式 extended_gcd で計算して検証した。

import random

def is_prime_miller_rabin(n, k=20):
    if n < 2:
        return False
    if n == 2 or n == 3:
        return True
    if n % 2 == 0:
        return False
    r, d = 0, n - 1
    while d % 2 == 0:
        r += 1
        d //= 2
    for _ in range(k):
        a = random.randrange(2, n - 1)
        x = pow(a, d, n)
        if x == 1 or x == n - 1:
            continue
        for _ in range(r - 1):
            x = pow(x, 2, n)
            if x == n - 1:
                break
        else:
            return False
    return True

def generate_prime(bits):
    while True:
        p = random.getrandbits(bits) | (1 << (bits - 1)) | 1
        if is_prime_miller_rabin(p):
            return p

def mod_inverse(a, m):
    g, x, _, steps = extended_gcd(a % m, m)  # 前節で定義した反復形式
    if g != 1:
        raise ValueError("モジュラ逆元が存在しません")
    return x % m, steps

p = generate_prime(1024)
q = generate_prime(1024)
n = p * q
phi = (p - 1) * (q - 1)
e = 65537
d, steps_used = mod_inverse(e, phi)

print(f"p bit length: {p.bit_length()}")
print(f"q bit length: {q.bit_length()}")
print(f"n bit length: {n.bit_length()}")
print(f"phi bit length: {phi.bit_length()}")
print(f"extended_gcd steps to compute d: {steps_used}")
print(f"e*d mod phi == 1: {(e * d) % phi == 1}")

message = 123456789012345678901234567890
c = pow(message, e, n)
m2 = pow(c, d, n)
print(f"message == decrypt(encrypt(message)): {message == m2}")

実行結果:

項目
\(p\) のビット長1024
\(q\) のビット長1024
\(N=pq\) のビット長2048
\(\varphi(N)\) のビット長2048
\(e\)65537
\(d\) を求めるための extended_gcd ステップ数9
\(e \cdot d \bmod \varphi(N) = 1\)True
end-to-end 検証(暗号化→復号で元のメッセージに一致)True

\(\varphi(N)\) が2048ビット(10進で600桁超)という巨大な数であっても、拡張ユークリッドの互除法はわずか 9ステップ(ラメの定理の境界 \(O(\log\varphi(N))\) 、実際 \(\log_\varphi(\varphi(N)) \approx 2984\) よりはるかに小さい。これは \(e=65537\) が \(\varphi(N)\) に比べて非常に小さいため、実効的なステップ数は \(\min(a,b)=e\) 側のビット長 \(17\) ビット程度に律速されるからである)で逆元 \(d\) を計算し、\(e\cdot d\equiv1\pmod{\varphi(N)}\) を満たすことが確認できた。さらに、この \(d\) を使って実際にメッセージを暗号化・復号すると元のメッセージに一致することも確認しており、拡張ユークリッドの互除法が単なる数学的存在証明にとどまらず、実用の暗号システムの鍵生成の中核を担っていることが分かる。

全体検証まとめ

本記事の主張を、実行したコードで数値的に裏付けた結果を以下にまとめる。

  1. ベズーの等式の検証: \(1 \le a,b \le 10^{12}\) からランダムに選んだ 200,000組全てについて、extended_gcd が返す \((d,x,y)\) が \(ax+by=d\) を厳密に満たし、かつ \(d\) が math.gcd(a,b) と一致することを確認(反例0件)。具体例として、教科書的な組 \((1071,462)\) では \(\gcd=21\) 、\(x=-3,y=7\) で \(1071\times(-3)+462\times7=21\) 。
  2. フィボナッチ組の最悪ケース性: 8〜256ビットの全区間で、フィボナッチ組のステップ数はランダム組平均の約2.3〜2.5倍。フィボナッチ組のステップ数とラメの定理の境界 \(\log_\varphi b\) の比は、ビット長が大きくなるほど1に収束(256ビットで0.999)。
  3. モジュラ逆元の応用: 2048ビット相当(1024ビット素数×2)の RSA スケールで、\(e=65537\) の \(\varphi(N)\) に関する逆元をわずか9ステップで計算し、\(e\cdot d\equiv1\pmod{\varphi(N)}\) と end-to-end の暗号化・復号一致を確認。
  4. 連分数との一致: \(355/113\) 、\(1071/462\) 、\(49/34\) の3例全てで、互除法の商列と連分数展開が完全一致。

関連記事

参考文献

  • Lamé, G. (1844). “Note sur la limite du nombre des divisions dans la recherche du plus grand commun diviseur entre deux nombres entiers”. Comptes Rendus de l’Académie des Sciences, 19, 867-870.
  • Knuth, D. E. (1997). The Art of Computer Programming, Volume 2: Seminumerical Algorithms (3rd ed.). Addison-Wesley. Section 4.5.3.
  • Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press. Chapter 31.