あめんばーどのバーチャル日記

バーチャルな世界で過ごして競プロしてる人の雑談

拡張ユークリッド互除法まとめ

基本的にはここで書いたModの割り算に関する言及の書き直しみたいなもんです。

【競プロ】個人的に優先度の高いライブラリ - あめんばーどのバーチャル日記

背景

Modの割り算にはその数の逆元を求める必要があり、それを求める方法としてフェルマーの小定理と、拡張ユークリッド互除法がある。

フェルマーの小定理

P素数ならa^{P-1}≡1 (mod P) なのでb≡a^{P-2}(mod P)は逆元である。これは繰り返し二乗法を使えばO(logP)

拡張ユークリッド互除法

今回言及する内容なので後述

二つの速度的な違い

どちらもO(logP)である。ただし例えばP=1000000007の場合、フェルマーの小定理は常に30回。対して拡張ユークリッド互除法は1回~36回(平均は17.932回)
これはフェルマーの小定理logの底が2に対し、ユークリッド互除法は最悪ケースがフィボナッチ数列の為\frac{1+\sqrt5}{2}なためである。

実際拡張ユークリッド互除法の最悪ケースのひとつ920386740の逆元を10^{7}回分求めると、この通り拡張ユークリッド互除法のほうが遅い

拡張ユークリッドの互除法:4121ms
フェルマーの小定理:3129ms

逆に2でやるとぶっちぎりで早い。

拡張ユークリッドの互除法:276ms
フェルマーの小定理:3185ms

期待値で考えるとやはりユークリッド互除法のほうが優れていそうである。とはいえ自分の中でも拡張ユークリッド互除法が曖昧だったのでいろいろ証明したりしたのでまとめた。って感じ

拡張ユークリッド互除法

コード

tuple<ll, ll, ll> extGcd(ll a, ll b) {
  if (b == 0) 
    return make_tuple(a, 1, 0);    // 行きがけのゴール地点
  auto [g, x, y] = extGcd(b, a % b);
  return make_tuple(g, y, x - y * (a / b));  // 帰りがけの動作
}

実装自体はこの4行で十分なのである。tupleはそれぞれGCDと下記を満たすx,yを返している。 g=ax+by

何してるのか

3行目はユークリッド互除法。 4行目は式を整理すれば返した値が成立していることがわかる

∃B=ak+b(B=b (mod a))に対して

y×B+(x-y×k)×a\\=y×(ak+b)+(x-y×k)×a\\=by+ax\\=g

と、うまい感じに成り立ってくれる。実際の数字で試しているのは前のブログ参照。

1行目と2行目はGCDが求まった瞬間にaはgcdなので、下記が絶対成り立ってくれる。

a×1+b×0\\=g×1+0×0\\=g

bの係数は0

そこで以前疑問に持たれたのが、bの係数は0である必要性である。もちろんax+by=gとなるx,yは無数にあるので、0にしなくても別の値が出てきてくれるだけなのだ。

だが0にすべきである。疑問に持たれた時や前回のブログでは「値が膨らまなさそう」と曖昧に言った。厳密には下記が成り立つからである

初期のbの係数を0にした場合、必ず以後|x|≦b及び|y|≦aが成立する

証明はシンプル。最初こそは成立しないが、その寸前の状態はその条件が成立しているはずである。

gk×0+g×1=g

ここで|x|≦b及び|y|≦aが成立しているのを仮定として機能的に示すために先程取り出したBを利用してこの式に戻ってこよう

B×y+(x-y×k)×a=g

これで|y|≦a及び|x-y×k|≦Bが成立していればいい。前者は仮定である。後者は三角不等式により

|x-y×k|≦|x|+|y×k|\\=|x|+k|y|≦b+ka\\=B

よって係数を0にすれば先程の条件は常に成り立つわけである。つまりオーバーフローしない。とてもうれしい。