問題
2つの巨大な非負整数 $A \ge B > 0$(最大300桁程度)の最大公約数 $\gcd(A,B)$ を求めよ。
普通のユークリッド互除法は正しいが、桁数が非常に大きいとき各ステップの`a % b`が低速になる。Lehmer's GCD Algorithmは、$A,B$の先頭の数十〜数百ビットだけを取り出した近似値$A_h,B_h$(機械語1語サイズ)を使って複数回分の商をまとめて計算し、$2\times2$整数行列として蓄積したうえで最後に1回だけ元の巨大な$A,B$に一括適用することで高速化する。
アルゴリズム概要(各反復):
- $B$のビット長が64ビット以下なら通常のユークリッド互除法で仕上げて終了。
- そうでなければ$A$の先頭64ビット相当を$A_h$、同じシフト量の$B$を$B_h$とする。
- 真の比$A/B$を挟み込む境界ペア$P=(A_h,B_h+1)$(比が最小)と$Q=(A_h+1,B_h)$(比が最大)で並行に互除法を進め、両方が同じ商を返す限り$2\times2$行列$(x_0,x_1,y_0,y_1)$に蓄積。商が食い違ったら打ち切り。
- 1つも安全に確定できなければ通常の互除法を1ステップだけ元の$A,B$に適用。そうでなければ蓄積した変換を一括適用。
- $B=0$になったら$A$が答え。
入力形式
A B
制約
$1 \le B \le A < 10^{300}$
10進文字列として入力
入出力例
入力例1
270 192
出力例1
6
入力例2
10946 6765
出力例2
1
10946, 6765は連続するフィボナッチ数。連続フィボナッチ数は常に互いに素であり、ユークリッド互除法が最悪ケースの反復回数になる有名な入力例でもある。
概念図
ヒント(段階的開示)
ヒント1: 方向性
Pythonの整数は多倍長対応だから`math.gcd`を呼べば済むが、この問題の目的はアルゴリズム自体を実装して理解すること。実世界の多倍長演算ライブラリ(GMPなど)が、なぜ単純なユークリッド互除法だけでは不十分で、こうした先頭桁の近似計算による高速化が必要になるのかを体感するのが狙い。
ヒント2: アプローチ
「$A,B$の下位ビットを見なくても、先頭の数ビットだけで商$q=\lfloor A/B\rfloor$が確定できる場合が多い」という点がポイント。ただし先頭ビットだけでは下位ビット次第で真の商とズレる危険がある。「真の$a/b$が取りうる最小の比と最大の比、その両極端で互除法を進めても同じ商になるなら、実際の下位ビットが何であっても安全にその商を採用できる」という判定がLehmer法の核心的な安全性チェック。
ヒント3: 誘導(コード骨格)
P0, P1 = A, B + 1 # 比が最小になる側の境界ペア
Q0, Q1 = A + 1, B # 比が最大になる側の境界ペア
x0, x1, y0, y1 = 1, 0, 0, 1
while P1 != 0 and Q1 != 0:
q1 = P0 // P1
q2 = Q0 // Q1
if q1 != q2:
break
q = q1
P0, P1 = P1, P0 - q * P1
Q0, Q1 = Q1, Q0 - q * Q1
x0, x1, y0, y1 = y0, y1, x0 - q * y0, x1 - q * y1
行列の更新は$x$側と$y$側を入れ替えながら行う(`x0,x1 = y0,y1`のように)点に要注意。単純に$x$は$x$だけ、$y$は$y$だけで独立更新すると、複数ステップ後に元の$a,b$への正しい変換行列にならない(Lehmer法で最もバグりやすい箇所)。
模範解答 (Python)
import sys
def lehmer_gcd(a, b):
WORD_BITS = 64
while b:
if b.bit_length() <= WORD_BITS:
while b:
a, b = b, a % b
return a
shift = max(0, a.bit_length() - WORD_BITS)
A = a >> shift
B = b >> shift
P0, P1 = A, B + 1
Q0, Q1 = A + 1, B
x0, x1, y0, y1 = 1, 0, 0, 1
while P1 != 0 and Q1 != 0:
q1 = P0 // P1
q2 = Q0 // Q1
if q1 != q2:
break
q = q1
P0, P1 = P1, P0 - q * P1
Q0, Q1 = Q1, Q0 - q * Q1
x0, x1, y0, y1 = y0, y1, x0 - q * y0, x1 - q * y1
if x1 == 0:
a, b = b, a % b
else:
a, b = x0 * a + x1 * b, y0 * a + y1 * b
return a
def solve():
A, B = map(int, sys.stdin.read().split())
print(lehmer_gcd(A, B))
solve()
計算量: 巨大整数演算の回数が $O(\log(\max(A,B)))$ から実用上大きく削減される(機械語サイズの近似計算で複数ステップをまとめるため)。
Step-by-Step 解説
1小さくなったら通常の互除法へ
`b.bit_length()<=WORD_BITS`なら普通のユークリッド互除法で仕上げ、最終的な正しさを保証。
`b.bit_length()<=WORD_BITS`なら普通のユークリッド互除法で仕上げ、最終的な正しさを保証。
2先頭64ビットの抽出
`a.bit_length()-WORD_BITS`だけ右シフトして$A$を得、$b$も同じシフト量で$B$を得る。
`a.bit_length()-WORD_BITS`だけ右シフトして$A$を得、$b$も同じシフト量で$B$を得る。
3境界ペアを使った安全性チェック
真の比$a/b$を挟み込む2つの境界ペア$P=(A,B+1)$,$Q=(A+1,B)$の商が一致する限り、その商は真の$a,b$に対しても正しい。
真の比$a/b$を挟み込む2つの境界ペア$P=(A,B+1)$,$Q=(A+1,B)$の商が一致する限り、その商は真の$a,b$に対しても正しい。
4蓄積した変換の一括適用
複数回分のステップを1回の巨大整数演算にまとめるのが高速化の本質。
複数回分のステップを1回の巨大整数演算にまとめるのが高速化の本質。
5フォールバック
`x1==0`(安全な商が1つも見つからない)場合は通常の互除法を1ステップだけ適用。
`x1==0`(安全な商が1つも見つからない)場合は通常の互除法を1ステップだけ適用。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 行列の更新で$x$側・$y$側を独立に更新してしまう | 「行と行を入れ替えながら更新する」必要性に気づかず`x0,x1=x1,x0-q*x1`のように自己完結で更新する | `x0,x1,y0,y1 = y0,y1, x0-q*y0, x1-q*y1`のように新しい$x$側は古い$y$側から計算する |
| A,Bの抽出シフト量を別々に計算 | 位取りがずれて商の近似が破綻 | `shift=a.bit_length()-WORD_BITS`という1つの値を両方に使う |
| 境界ペアの片方(Pのみ)しか見ない | 商が下位ビット次第で変わるケースを見逃す | P側・Q側両方の境界から計算した商が完全一致することを確認 |
| 閾値を小さくしすぎて高速化の恩恵が出ない | ガード桁が不足 | 実用的には32〜64ビット程度の閾値を確保 |
次のステップ
- 発展: 100桁・300桁のランダムな大整数ペアで`math.gcd`との一致を大量検証してから、素朴な互除法とLehmer法の`a%b`呼び出し回数を比較する
- 発展: 拡張ユークリッド互除法版($ax+by=\gcd(a,b)$)にLehmer法を組み込む
- 次回予告: 3-NTT素数によるCRT合成(Garnerのアルゴリズム)