0. はじめに
2020年9月7日にAtCoder公式のアルゴリズム集 AtCoder Library (ACL)が公開されました。
私はACLに収録されているアルゴリズムのほとんどが初見だったのでいい機会だと思い、アルゴリズムの勉強からPythonでの実装までを行いました。
この記事では internal_math をみていきます。
internal_mathはACLの内部で使われる数論的アルゴリズムの詰め合わせで内容は以下の通りです。
名称 | 概要 |
---|---|
safe_mod | 整数 |
barrett | 高速な剰余演算。 |
pow_mod | |
is_prime | 高速な素数判定。 |
inv_gcd | 整数 |
primitive_root | |
floor_sum_unsigned |
本記事ではこれらの内、
- safe_mod
- pow_mod
- inv_gcd
- barrett
を扱います。なお、constexpr(定数式)自体については触れません。
本記事で扱わない
- is_prime
- primitive_root
については以下の記事で扱っています。よろしければそちらもご覧ください。
【internal_math編②】AtCoder Library 解読 〜Pythonでの実装まで〜
なお、floor_sum_unsigned は math 内の floor_sum の補助的な役割なので説明は
【math編】AtCoder Library 解読 〜Pythonでの実装まで〜
で行なっています。
対象としている読者
- ACLの内部で使われている数学系アルゴリズムを知りたい方。
- ACLのコードを見てみたけど何をしているのかわからない方。
- C++はわからないのでPythonで読み進めたい方。
参考にしたもの
C++のドキュメントです。
@drkenさんによる、競技プログラミングで問われるあまりの求め方がまとめられた記事です。非常にわかりやすいです。
ユークリッドの互除法を数式で記述する参考にしました。
Barrett reduction の参考記事です。
1. safe_mod
整数
1.1. C++での剰余演算
C++では以下のようになります。
#include <bits/stdc++.h>
using namespace std;
int main(){
cout << " 7 % 3 = " << 7 % 3 << endl; // 7 % 3 = 1
cout << "-7 % 3 = " << -7 % 3 << endl; // -7 % 3 = -1
return 0;
}
注目するのは2つ目で、
- 商は0に向かって丸め込まれる
を満たす
によるものです。(参考)
1.2. 実装
safe_mod は剰余
Pythonでの実装は以下のようになります。
def safe_mod(x, m):
x %= m
if x < 0: x += m
return x
なお、Pythonでは正整数
# safe_modによる剰余演算
print(f'safe_mod(7, 3) = {safe_mod(7, 3)}') # safe_mod(7, 3) = 1
print(f'safe_mod(-7, 3) = {safe_mod(-7, 3)}') # safe_mod(-7, 3) = 2
# 算術演算子%による剰余演算
print(f' 7 % 3 = {7 % 3}') # 7 % 3 = 1
print(f'-7 % 3 = {-7 % 3}') # -7 % 3 = 2
2. pow_mod
整数
2.1. 素朴な方法
指数
def naive_pow_mod(x, n, m):
r = 1 # 結果を入れる変数
for _ in range(n):
r *= x
r %= m # mで割ったあまりを求める
return r
x = 3
n = 4
m = 5
print(naive_pow_mod(x, n, m)) # 1
しかし、
回程度の乗算が必要になる- 計算の過程で値が非常に大きくなる
これらによって非常に長い計算時間が必要になります。そしてこれらの問題点を解決することで高速に
2.2. 問題点1の解決策: 繰り返し二乗法
繰り返し二乗法(バイナリ法とも呼ばれます)はその名の通り二乗を繰り返します。これによって大きな指数の計算を少ない計算回数で行えます。
を二乗して を得る。 を二乗して を計算することなく を得る。 を二乗して を計算することなく を得る。
という具合です。具体例として
なので
とかけます。よって、
を求めるまでに5回- それらを掛け合わせる3回(結果の値として1を用意した場合)
の合計8回の乗算で求められます。
これらの手続きを機械的に行うためには二進数を活用するのが有効です。二進数の定義からいって当たり前ですが、整数
Pythonでの実装は以下のようになります。
# 繰り返し二乗法によるべき乗計算
def binary_pow_mod(x, n, m):
r = 1 # 結果を入れる変数
y = x # x^(2のべき乗) を入れる変数
while n:
if n & 1: r = r * y #最下位ビットが1なら乗算する
y = y * y
n >>= 1 #右シフトで次のビットを見る
r %= m
return r
2.3. 問題点2の解決策: mod演算の性質
コンピューターは(人間から見て)非常に高速に計算でき、またPythonではメモリが許す限り大きな整数を扱うことができますが、それでもやはり大きな桁の計算は時間がかかります。例えば
を計算することになります。もし欲しいものが
乗算は(最後にmodを取りさえすれば)いつ何度でもmodをとって良い
2.4. 実装
pow_modは繰り返し二乗法にmod演算の性質を使ったものです。これは先ほど実装したbinary_pow_modに少し手を加えることで実装できます。
def pow_mod(x, n, m):
if m == 1: return 0 # 1で割った余りは常に0
r = 1
y = x % m # mで割ったあまりにする
while n:
if n & 1: r = r * y % m # mで割ったあまりにする
y = y * y % m # mで割ったあまりにする
n >>= 1
return r
なお、Pythonでは組み込み関数のpow()がpow_modに相当するのでこの実装は不要です。
# 素朴な方法での実装
print(naive_pow_mod(3, 4, 5)) # 1
#print(naive_pow_mod(13, 1000000, 1000000007)) # 終わりません
# 繰り返し二乗法を用いた実装
print(binary_pow_mod(13, 1000000, 1000000007)) # 735092405 このくらいならなんとか計算できます
#print(binary_pow_mod(13, 1000000000, 1000000007)) # 終わりません
# 繰り返し二乗法 + modの性質を用いた実装(ACLのpow_modに相当)
print(pow_mod(13, 1000000000, 1000000007)) # 94858115
# Pythonの組み込み関数powを用いた計算
print(pow(13, 1000000000, 1000000007)) # 94858115
3. inv_gcd
整数
- 最大公約数
となる
を計算します。ただし
3.1. ユークリッドの互除法
まずは
最大公約数を計算するアルゴリズムとしてユークリッドの互除法というものがあります。
Pythonでの実装は以下のようになります。
def gcd(a, b):
if b == 0: return a
return gcd(b, a % b)
以降では
最大公約数がユークリッドの互除法によって計算できるのは以下の2つによるものです。
に対し
1.の証明は@drkenさんの記事にありますのでそちらをご覧ください。
2.は
1.の主張は強力です。いま
となります。また、
3.2. ユークリッドの互除法を数式で記述する
ユークリッドの互除法を数式で表します。
となり、このようにして得られる
上の式を行列を用いて表現すると
となります。これらをまとめて書くと
が得られます。
いま、
とおくと行列式は
なので、逆行列
です。したがって式(
となります。
3.3. 拡張ユークリッドの互除法
さて、inv_gcdで得られる2つ目のもの
となる
を見ていきましょう。これは整数
と書くこともできます。よって、この式を満たす
ところで、前節で得られた式
とおくと
となります。つまり、この
を満たします。したがって、式
3.4. inv_gcd実装の準備①
ユークリッドの互除法は
を
を同時に計算することで
では実際に計算する過程を見ていきましょう。
であることがわかります。
を満たすためです。
次に各変数の従う漸化式を見ていきます。
まず、
次にこれを用いて他の変数の遷移がわかります。
終了条件は
となります。
3.5. inv_gcd実装の準備②
そのためにまず、以下を示します。
が成り立つ。
用いるのは
と絶対値の性質
です。
数学的帰納法により示します。
で満たします。
を満たすと仮定すると
となり満たします。
以上より、
が成り立つことが示されました。
この結果を使って
となります。先ほど示した不等式において
であり、
を満たすことが示されました。
また、
とすることで
より確かに求める解になっています。ここで、
3.6. inv_gcd実装の準備③
inv_gcd実装の準備①では各変数に添字をつけて数列のようにそれぞれの遷移を追ってきましたが、実装上は古い値を保持する必要はないのでよりコンパクトに書くことができます。
ここからはACLで実際に使われている変数名に沿っていきます。
まずは必要な変数を確認しましょう。
準備①でも述べたとおり、
で終わりです。よって、以降では
初期状態は
です。ここから、
3.7. 実装
前節で述べた通りに実装していきます。なお、ACLにおいて safe_mod を用いている部分はPythonにおいて同等の機能である算術演算子 % で代用します。
def inv_gcd(a, b):
a %= b
if a == 0: return b, 0
# 初期状態
s, t = b, a
m0, m1 = 0, 1
while t:
# 遷移の準備
u = s // t
# 遷移
s -= t * u
m0 -= m1 * u
# swap
s, t = t, s
m0, m1 = m1, m0
if m0 < 0: m0 += b // s
return s, m0
print(inv_gcd(3, 5)) # (1, 2)
print(inv_gcd(20, 15)) # (5, 1)
4. barrett
「ある自然数
ACLにおいては
を高速化する目的で使われています。
以降では
4.1. アイデア
いくら割り算が苦手と言っても、あまりを求める際には避けては通れません。なぜならあまりは
と表されるからです。また、一般的な数による割り算は苦手でも2のべき乗による割り算は得意です。2進数で動くコンピューターにとって、「
よって、あまりを求める演算を高速化するために、苦手な演算(一般的な自然数による割り算)を得意な演算(足し算、引き算、掛け算、シフト演算)に置き換えることを考えます。
「
と近似できたとします。すると、
となり、得意な演算(引き算、掛け算、シフト演算)だけであまりを表すことができました。
4.2. k, n の決め方
では、自然数
を満たす自然数
まず、
となるように選ぶ必要があります。
となるように選ぶだけです。よって
となります。ACLでは天井関数を採用していますが、一般的には床関数を選ぶことが多いようです。
さて、
を導入します。具体例として競技プログラミングでよく問われる
です。そこで、
ACLでは
と決まります。
補足1
となっています。
ACLにおいて入力
となるので問題ありません。よって以降では
補足2
割り算が入っていると思われるかもしれませんが、いま
4.3. なぜ k=64 ?
では、なぜ
ひとつの理由として unsigned long long (符号なし8バイト整数)で扱える最大値が
そしてもうひとつの理由としては、
これを示すためには以下のことを示せば良いでしょう。
と表されたとき、次の関係式が成立する。
ここで、
であり、>>は右シフト演算である。
これが示された場合、
となります。すなわち
それでは証明していきます。
まずは下限から見ていきます。いま、
であるから、
したがって、
となります。
つづいて、上限を見ていきます。
とかけることを利用すると
が得られます。ここで、
となるので
したがって、
が成り立ちます。
以上より
が示されました。
4.4. 実装
では実装していきます。まずクラスBarrettを作成しコンストラクタを実装します。法
class Barrett:
# @param 1 <= m < 2^31
def __init__(self, m):
self._m = m
self.im = -(-(1<<64) // m) if m > 1 else 0
# mを返すメソッド
def umod(self):
return self._m
変数im
がこれまでの
このように
ではメソッド "mul" を実装します。これが
class Barrett:
# __init__()
# umod()
def mul(self, a, b):
assert 0 <= a < self._m
assert 0 <= b < self._m
z = a * b
v = z - ((z * self.im)>>64) * self._m
if v < 0: v += self._m
return v
m = 1000000007
bt = Barrett(m)
a = 12345678
b = 87654321
print(bt.mul(a, b)) # 14799574
print((a * b) % m) # 14799574
4.5. 実用性は...
察している方もいるかと思いますが、 Pythonでは組み込みの算術演算子 % を素直に使った方が速いです。
巨大な数になれば効果が出てくるかもしれませんが、競技プログラミングで扱うような数では(Pythonでは)必要なさそうです。
5. おわりに
今回はACLの内部で使われるアルゴリズムを見てきました。数式を追うのが大変でしたが、理解が深まりました。
internal_mathのなかで今回触れなかったものについてはinternal_math編②で書いていますので、よろしければそちらもご覧ください。
説明の間違いやバグ、アドバイス等ありましたらお知らせください。
コメント
いいね以上の気持ちはコメントで