離散対数。
n を法とする dlogb(a)。
bz≡a(modn) なる z≥0 が存在すれば、そのうち最小のものを返す。
0≤b,a<n とする。
また、00=1 とする。
コーナーケースに注意。
n=1 なら 00=1≡0 より z=0 を返す。
以下、n>1 とする。
- dlog∙(1)=0
- dlog0(0)=1
上記に該当しないとき、dlog0(∙) と dlog1(∙)
は存在しない。残りのケースについては、以下の方針に基づいて求める。
1→b→b2→…(modn) でできる functional graph を考えると、ρ
の字状になっていることに気づく。しっぽの長さ μ は高々 log(n)、頭の長さ
λ は φ(n) の約数になっていることが示せる。
そこで、まずしっぽの部分に解があるかを愚直に O(μ) 時間で調べる。
見つからなければ、頭の部分に解があるかを o(λ) 時間で調べる。
O(λlog(λ)) 時間のアルゴリズムを紹介する。
ρ の頭の部分は bμ→bμ+1→⋯→bμ+λ≡bμ
である。このとき、∀i≥μ について
bi+1⋅bλ−1≡bi(modn) が成り立つ。
すなわち、bλ−1 を掛けることで、→ をひとつ戻ることができる1。
また、頭の部分に a が含まれるためには gcd(bμ,n)=gcd(a,n)
が必要であり、これが成り立たなければ解なしを報告すればよい。
よって、以下ではこれが成り立つとする。
z=μ+i⋅⌊λ⌋+j (0≤j<λ)
とし、(i,j) を探すことを考える。
bμ+i⋅⌊λ⌋+jbμ+i⋅⌊λ⌋+j⋅(bλ−1)jbμ+i⋅⌊λ⌋≡a≡a⋅(bλ−1)j≡a⋅(bλ−1)j
これより、i と j を分離でき、a⋅(bλ−1)j↦j
の連想配列を作っておくことで、bμ+i⋅⌊λ⌋
に対応する要素があれば返せる。ただし、複数の j について
a⋅(bλ−1)j が同じ値を取りうるので注意する必要がある。
j による小さい幅で連想配列を作り、i による大きい幅でそれにアクセスする様子から
baby-step giant-step algorithm と呼ばれている。
μ と λ を求める方法について述べる。
自然数 x が素数 p で割り切れる回数、すなわち x の素因数分解における p
の指数を πp(x) で表すとする2。
b=∏pi: primepiei とすると、次が成り立つ。
μ=imax⌈eiπpi(n)⌉.
また、p=∏ipiπpi(n), n′=n/p とする。すなわち、n
を b の各素因数で割り切れるだけ割ったものを n′ とする。このとき、λ
は φ(n′) の約数3であり、次の式に基づき愚直に求める4。
λ=i⊑φ(n′)minis.t.bμ⋅bi≡bμ(modn).
φ(n′) の約数の個数は o(n/log(n))
なので、繰り返し二乗法を用いて O(n) 時間でできる。
実際には、解が見つかった時点で打ち切れるので、O(λ+∑i⊑φ(n′)i≤λlog(i)) 時間になる。前半の λ
は試し割り法に由来する項なので、約数列挙を O(1) delay でできるなら消去できる。
まず、λ が φ(n′) の約数になることを示す。
bμ における pi の指数は ei⋅maxj⌈πpj(n)/ej⌉
であり、特に ei⋅⌈πpi(n)/ei⌉≥πpi(n) 以上である。
よって、bμ は p で割り切れ、bμ≡0(modp) である。これより、bμ+i≡0(modp) (i≥0) である。
また、b と n′ は互いに素なので、Euler の定理より bφ(n′)≡1(modn′)
が成り立つ。よって、bμ+φ(n′)≡bμ(modn′)
である。上記の p に関する結果と合わせて bμ+φ(n′)≡bμ(modn)
を得る。よって、λ が φ(n′) の約数であることが示された。
次に、μ の値が正しいことを示す。bμ が頭の部分に含まれることは上で示した
bμ+φ(n′)≡bμ(modn) より従うので、μ>0 のとき
bμ−1 が頭の部分に含まれないことを示せば十分である。
μ の定義より、ある i に対して ei⋅(μ−1)<πpi(n) が成り立つ。
よって bμ−1 は p は割り切れず、bμ−1≡0(modn) である。
これにより、頭の部分に含まれるどの要素とも等しくないことがわかる。
最後に、gcd(bμ,n)=gcd(a,n) であることが、頭の部分に a が存在する、すなわち
∃i≥0:bμ+i≡a(modn) であることの必要条件であることを示す。
- gcd(bμ+i)≡0(modp) for i≥0
- gcd(b,n′)=1 より gcd(bμ+i,n′)=1 for i≥0
- gcd(p,n′)=1
- n=p⋅n′
より、gcd(bμ+i,n)=p (i≥0) で一定となる。
すなわち、gcd(a,n)=p が必要となる。
O(n+λ⋅H(λ)) time.
ただし、H(n) は要素数 n の HashMap
の insert
と get
にかかる時間とする。
BS/GS パートでは BTreeMap
を用いるよりも HashMap
を用いた方が高速だったので、とりあえずそうした。
ここで、λ は最悪 Θ(n) である。
また、篩の前計算などで n, b の素因数分解と φ(n) の約数列挙を O(1) delay
でできるなら n の項をそれらの個数に落とせる。
λ を求める際に bμ+i≡bμ なる最小の i⊑φ(n′)
を探したが、実際には Euler の定理ではなく Carmichael の定理に基づいて
λ(n′)
の約数を探せばよい。関数値を求めるの自体は φ(∙) よりも λ(∙)
の方が若干複雑だが、全体では高速に動作したので後者を選択した。
use nekolib::math::DLog;
assert_eq!(6_u64.dlog(5, 13), Some(9));
assert_eq!(27_u64.dlog(3, 30), Some(3));
assert_eq!(2_u64.dlog(0, 4), Some(2));
assert_eq!(0_u64.dlog(1, 2), Some(0));
assert_eq!(2_u64.dlog(3, 10), None);
内部で Euler の φ 関数 Carmichael の λ
関数と約数列挙と素因数分解が必要となるので、
篩などを用いて高速化が図れる場合に対応しやすいようにしてみるとよい?
ⓘpub fn dlog<I, J>(
b: u64, a: u64, n: u64,
carmichael_lambda: impl Fn(u64) -> u64,
divisors: impl Fn(u64) -> I,
factors: impl Fn(u64) -> J,
) -> Option<u64>
where
I: Iterator<Item = u64>,
J: Iterator<Item = (u64, u32)>,
{ ... }
一方で、そうでない場合にわざわざ関数を渡す必要があるので単に面倒かも。
お手軽パターンと二つ用意するといいかも?(デフォルト引数欲しいね)
とはいえ、結局 BS/GS パートが律速になってしまいそう。
関数名は discrete logarithm、引数 b は base、a は antilogarithm から。